add ESLint auto-fixable changes (quotes, semi...)

This commit is contained in:
Amri Toufali 2022-05-10 16:40:26 -07:00
Родитель 7112d08b01
Коммит 344e394bb0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 75269D7487754F5D
123 изменённых файлов: 6681 добавлений и 7012 удалений

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
// See https://jestjs.io/docs/en/manual-mocks
function MessageValidator() {}
function MessageValidator () {}
MessageValidator.prototype.validate = function (hash, cb) {
if (hash.Signature === "invalid") {
cb(new Error("The message signature is invalid."));
return;
if (hash.Signature === 'invalid') {
cb(new Error('The message signature is invalid.'))
return
}
cb(null, hash);
};
cb(null, hash)
}
module.exports = MessageValidator;
module.exports = MessageValidator

Просмотреть файл

@ -1,79 +1,79 @@
"use strict";
'use strict'
/* eslint-disable no-process-env */
const path = require("path");
require("dotenv").config({path: path.join(__dirname, ".env")});
const path = require('path')
require('dotenv').config({ path: path.join(__dirname, '.env') })
const requiredEnvVars = [
"NODE_ENV",
"SERVER_URL",
"PORT",
"LOGOS_ORIGIN",
"COOKIE_SECRET",
"SESSION_DURATION_HOURS",
"SMTP_URL",
"EMAIL_FROM",
"SES_CONFIG_SET",
"SES_NOTIFICATION_LOG_ONLY",
"BREACH_RESOLUTION_ENABLED",
"FXA_ENABLED",
"FXA_SETTINGS_URL",
"FX_REMOTE_SETTINGS_WRITER_SERVER",
"FX_REMOTE_SETTINGS_WRITER_USER",
"FX_REMOTE_SETTINGS_WRITER_PASS",
"MOZLOG_FMT",
"MOZLOG_LEVEL",
"OAUTH_AUTHORIZATION_URI",
"OAUTH_TOKEN_URI",
"OAUTH_PROFILE_URI",
"OAUTH_CLIENT_ID",
"OAUTH_CLIENT_SECRET",
"HIBP_KANON_API_ROOT",
"HIBP_KANON_API_TOKEN",
"HIBP_API_ROOT",
"HIBP_RELOAD_BREACHES_TIMER",
"HIBP_THROTTLE_DELAY",
"HIBP_THROTTLE_MAX_TRIES",
"HIBP_NOTIFY_TOKEN",
"DATABASE_URL",
"PAGE_TOKEN_TIMER",
"PRODUCT_PROMOS_ENABLED",
"REDIS_URL",
"SENTRY_DSN",
"DELETE_UNVERIFIED_SUBSCRIBERS_TIMER",
"EXPERIMENT_ACTIVE",
"MAX_NUM_ADDRESSES",
];
'NODE_ENV',
'SERVER_URL',
'PORT',
'LOGOS_ORIGIN',
'COOKIE_SECRET',
'SESSION_DURATION_HOURS',
'SMTP_URL',
'EMAIL_FROM',
'SES_CONFIG_SET',
'SES_NOTIFICATION_LOG_ONLY',
'BREACH_RESOLUTION_ENABLED',
'FXA_ENABLED',
'FXA_SETTINGS_URL',
'FX_REMOTE_SETTINGS_WRITER_SERVER',
'FX_REMOTE_SETTINGS_WRITER_USER',
'FX_REMOTE_SETTINGS_WRITER_PASS',
'MOZLOG_FMT',
'MOZLOG_LEVEL',
'OAUTH_AUTHORIZATION_URI',
'OAUTH_TOKEN_URI',
'OAUTH_PROFILE_URI',
'OAUTH_CLIENT_ID',
'OAUTH_CLIENT_SECRET',
'HIBP_KANON_API_ROOT',
'HIBP_KANON_API_TOKEN',
'HIBP_API_ROOT',
'HIBP_RELOAD_BREACHES_TIMER',
'HIBP_THROTTLE_DELAY',
'HIBP_THROTTLE_MAX_TRIES',
'HIBP_NOTIFY_TOKEN',
'DATABASE_URL',
'PAGE_TOKEN_TIMER',
'PRODUCT_PROMOS_ENABLED',
'REDIS_URL',
'SENTRY_DSN',
'DELETE_UNVERIFIED_SUBSCRIBERS_TIMER',
'EXPERIMENT_ACTIVE',
'MAX_NUM_ADDRESSES'
]
const optionalEnvVars = [
"RECRUITMENT_BANNER_LINK",
"RECRUITMENT_BANNER_TEXT",
"GEOIP_GEOLITE2_PATH",
"GEOIP_GEOLITE2_CITY_FILENAME",
"GEOIP_GEOLITE2_COUNTRY_FILENAME",
"VPN_PROMO_BLOCKED_LOCALES",
"EDUCATION_VIDEO_URL_RELAY",
"EDUCATION_VIDEO_URL_VPN",
];
'RECRUITMENT_BANNER_LINK',
'RECRUITMENT_BANNER_TEXT',
'GEOIP_GEOLITE2_PATH',
'GEOIP_GEOLITE2_CITY_FILENAME',
'GEOIP_GEOLITE2_COUNTRY_FILENAME',
'VPN_PROMO_BLOCKED_LOCALES',
'EDUCATION_VIDEO_URL_RELAY',
'EDUCATION_VIDEO_URL_VPN'
]
const AppConstants = { };
const AppConstants = { }
if (!process.env.SERVER_URL && process.env.NODE_ENV === "heroku") {
process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`;
if (!process.env.SERVER_URL && process.env.NODE_ENV === 'heroku') {
process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
}
for (const v of requiredEnvVars) {
if (process.env[v] === undefined) {
throw new Error(`Required environment variable was not set: ${v}`);
throw new Error(`Required environment variable was not set: ${v}`)
}
AppConstants[v] = process.env[v];
AppConstants[v] = process.env[v]
}
optionalEnvVars.forEach(key => {
if (key in process.env) AppConstants[key] = process.env[key];
});
if (key in process.env) AppConstants[key] = process.env[key]
})
AppConstants.VPN_PROMO_BLOCKED_LOCALES = AppConstants.VPN_PROMO_BLOCKED_LOCALES?.split(",");
AppConstants.VPN_PROMO_BLOCKED_LOCALES = AppConstants.VPN_PROMO_BLOCKED_LOCALES?.split(',')
module.exports = Object.freeze(AppConstants);
module.exports = Object.freeze(AppConstants)

Просмотреть файл

@ -1,64 +1,64 @@
"use strict";
'use strict'
const HIBP = require("../hibp");
const DB = require("../db/DB");
const { changePWLinks } = require("../lib/changePWLinks");
const { getAllEmailsAndBreaches } = require("./user");
const HIBP = require('../hibp')
const DB = require('../db/DB')
const { changePWLinks } = require('../lib/changePWLinks')
const { getAllEmailsAndBreaches } = require('./user')
async function getBreachDetail(req, res) {
const allBreaches = req.app.locals.breaches;
const breachName = req.params.breachName;
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName);
async function getBreachDetail (req, res) {
const allBreaches = req.app.locals.breaches
const breachName = req.params.breachName
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName)
if (!featuredBreach) {
return res.redirect("/");
return res.redirect('/')
}
const affectedEmails = [];
const affectedEmails = []
if (req.session && req.session.user) {
const user = await DB.getSubscriberById(req.session.user.id);
req.session.user = user;
const user = await DB.getSubscriberById(req.session.user.id)
req.session.user = user
const allEmailsAndBreaches = await getAllEmailsAndBreaches(req.session.user, allBreaches);
const allEmailsAndBreaches = await getAllEmailsAndBreaches(req.session.user, allBreaches)
for (const verifiedEmail of allEmailsAndBreaches.verifiedEmails) {
for (const breach of verifiedEmail.breaches) {
if (breach.Name === breachName) {
affectedEmails.push({
emailAddress: verifiedEmail.email,
recencyIndex: breach.recencyIndex,
isResolved: breach.IsResolved,
});
isResolved: breach.IsResolved
})
}
}
}
}
const changePWLink = getChangePWLink(featuredBreach);
res.render("breach-detail", {
title: req.fluentFormat("home-title"),
const changePWLink = getChangePWLink(featuredBreach)
res.render('breach-detail', {
title: req.fluentFormat('home-title'),
featuredBreach,
changePWLink,
affectedEmails,
});
affectedEmails
})
}
function getChangePWLink(breach) {
if (!breach.DataClasses.includes("passwords")) {
return "";
function getChangePWLink (breach) {
if (!breach.DataClasses.includes('passwords')) {
return ''
}
if (changePWLinks.hasOwnProperty(breach.Name)) {
return changePWLinks[breach.Name];
return changePWLinks[breach.Name]
}
if (breach.Domain) {
return "https://www." + breach.Domain;
return 'https://www.' + breach.Domain
}
return "";
return ''
}
module.exports = {
getBreachDetail,
};
getBreachDetail
}

Просмотреть файл

@ -1,58 +1,55 @@
"use strict";
'use strict'
const fs = require("fs");
const path = require("path");
const fs = require('fs')
const path = require('path')
const AppConstants = require("../app-constants");
const mozlog = require("../log");
const {version, homepage, supportedLocales} = require("../package.json");
const AppConstants = require('../app-constants')
const mozlog = require('../log')
const { version, homepage, supportedLocales } = require('../package.json')
const log = mozlog("controllers.dockerflow");
const versionJsonPath = path.join(__dirname, "..", "version.json");
const log = mozlog('controllers.dockerflow')
const versionJsonPath = path.join(__dirname, '..', 'version.json')
// If the version.json file already exists (e.g., created by circle + docker),
// don't need to generate it
if (!fs.existsSync(versionJsonPath)) {
log.info("generating");
let commit;
log.info('generating')
let commit
try {
commit = require("git-rev-sync").short();
commit = require('git-rev-sync').short()
} catch (err) {
log.error("generating", {err: err});
log.error('generating', { err })
}
const versionJson = {
commit,
source: homepage,
version,
languages: supportedLocales,
};
languages: supportedLocales
}
fs.writeFileSync(versionJsonPath, JSON.stringify(versionJson, null, 2) + "\n");
fs.writeFileSync(versionJsonPath, JSON.stringify(versionJson, null, 2) + '\n')
}
function vers (req, res) {
if (AppConstants.NODE_ENV === "heroku") {
if (AppConstants.NODE_ENV === 'heroku') {
/* eslint-disable no-process-env */
return res.json({
commit: process.env.HEROKU_SLUG_COMMIT,
version: process.env.HEROKU_SLUG_COMMIT,
source: homepage,
languages: "*",
});
languages: '*'
})
/* eslint-enable no-process-env */
}
return res.sendFile(versionJsonPath);
return res.sendFile(versionJsonPath)
}
function heartbeat (req, res) {
return res.send("OK");
return res.send('OK')
}
module.exports = {
vers,
heartbeat,
};
heartbeat
}

Просмотреть файл

@ -1,114 +1,114 @@
"use strict";
'use strict'
const EmailUtils = require("../email-utils");
const AppConstants = require("../app-constants");
const path = require("path");
const { readdir } = require("fs/promises");
const partialDir = path.join(path.dirname(require.main.filename), "/views/partials/email_partials");
const EmailUtils = require('../email-utils')
const AppConstants = require('../app-constants')
const path = require('path')
const { readdir } = require('fs/promises')
const partialDir = path.join(path.dirname(require.main.filename), '/views/partials/email_partials')
let partialFilenames;
let partialFilenames
async function getPartialFilenames() {
async function getPartialFilenames () {
try {
partialFilenames = await readdir(partialDir);
partialFilenames = await readdir(partialDir)
} catch (e) {
console.error(e);
partialFilenames = [];
console.error(e)
partialFilenames = []
}
return partialFilenames;
return partialFilenames
}
async function getEmailMockUps(req, res) {
const email = "example@email.com";
const partials = partialFilenames || await getPartialFilenames();
async function getEmailMockUps (req, res) {
const email = 'example@email.com'
const partials = partialFilenames || await getPartialFilenames()
if (!["dev", "heroku"].includes(AppConstants.NODE_ENV)) return notFound(req, res);
if (!['dev', 'heroku'].includes(AppConstants.NODE_ENV)) return notFound(req, res)
if (!req.query.partial) {
req.query.partial = "email_verify";
req.query.type = "email_verify";
req.query.partial = 'email_verify'
req.query.type = 'email_verify'
}
if (!partials.includes(`${req.query.partial}.hbs`)) return notFound(req, res);
if (!partials.includes(`${req.query.partial}.hbs`)) return notFound(req, res)
if (["breachAlert", "pre-fxa", "singleBreach", "multipleBreaches", "noBreaches", "email_verify"].indexOf(req.query.type) === -1) {
return res.redirect("/email-l10n");
if (['breachAlert', 'pre-fxa', 'singleBreach', 'multipleBreaches', 'noBreaches', 'email_verify'].indexOf(req.query.type) === -1) {
return res.redirect('/email-l10n')
}
const unsafeBreachesForEmail = [];
["Dropbox", "Apollo", "Adobe"].forEach(name => {
unsafeBreachesForEmail.push(req.app.locals.breaches.filter(breach => breach.Name === name)[0]);
});
['Dropbox', 'Apollo', 'Adobe'].forEach(name => {
unsafeBreachesForEmail.push(req.app.locals.breaches.filter(breach => breach.Name === name)[0])
})
const emailContent = ((req) => {
switch(req.query.type) {
case "pre-fxa":
switch (req.query.type) {
case 'pre-fxa':
return {
emailSubject: req.fluentFormat("pre-fxa-subject"),
emailSubject: req.fluentFormat('pre-fxa-subject'),
preFxaEmail: true,
breachAlert: null,
};
case "noBreaches":
breachAlert: null
}
case 'noBreaches':
return {
emailSubject: req.fluentFormat("email-subject-no-breaches"),
emailSubject: req.fluentFormat('email-subject-no-breaches'),
breachAlert: null,
unsafeBreachesForEmail: [],
};
case "breachAlert":
unsafeBreachesForEmail: []
}
case 'breachAlert':
return {
emailSubject: req.fluentFormat("breach-alert-subject"),
breachAlert: req.app.locals.breaches.filter(breach => breach.Name === "LinkedIn")[0],
emailSubject: req.fluentFormat('breach-alert-subject'),
breachAlert: req.app.locals.breaches.filter(breach => breach.Name === 'LinkedIn')[0],
unsafeBreachesForEmail: null,
preFxaSubscriber: true,
};
case "email_verify":
preFxaSubscriber: true
}
case 'email_verify':
return {
emailSubject: req.fluentFormat("email-subject-verify"),
emailSubject: req.fluentFormat('email-subject-verify'),
breachAlert: null,
unsafeBreachesForEmail: null,
};
case "multipleBreaches":
unsafeBreachesForEmail: null
}
case 'multipleBreaches':
return {
emailSubject: req.fluentFormat("email-subject-found-breaches"),
unsafeBreachesForEmail: unsafeBreachesForEmail,
breachAlert: null,
};
emailSubject: req.fluentFormat('email-subject-found-breaches'),
unsafeBreachesForEmail,
breachAlert: null
}
default:
return {
emailSubject: req.fluentFormat("email-subject-found-breaches"),
emailSubject: req.fluentFormat('email-subject-found-breaches'),
unsafeBreachesForEmail: unsafeBreachesForEmail.slice(0, 1),
breachAlert: null,
};
breachAlert: null
}
}
})(req);
})(req)
res.render("email_l10n", {
layout: "email_l10n_mockups.hbs",
res.render('email_l10n', {
layout: 'email_l10n_mockups.hbs',
unsafeBreachesForEmail: emailContent.unsafeBreachesForEmail,
supportedLocales: req.supportedLocales,
whichPartial: `email_partials/${req.query.partial}`,
breachedEmail: "breachedEmail@testing.com",
recipientEmail: "recipientEmail@testing.com",
breachedEmail: 'breachedEmail@testing.com',
recipientEmail: 'recipientEmail@testing.com',
breachAlert: emailContent.breachAlert,
emailSubject: emailContent.emailSubject,
preFxaSubscriber: emailContent.preFxaSubscriber,
email,
preFxaEmail: emailContent.preFxaEmail,
ctaHref: EmailUtils.getEmailCtaHref("breach-alert", "go-to-dashboard"),
});
ctaHref: EmailUtils.getEmailCtaHref('breach-alert', 'go-to-dashboard')
})
}
function notFound(req, res) {
res.status(404);
res.render("subpage", {
analyticsID: "error",
headline: req.fluentFormat("error-headline"),
subhead: req.fluentFormat("home-not-found"),
});
function notFound (req, res) {
res.status(404)
res.render('subpage', {
analyticsID: 'error',
headline: req.fluentFormat('error-headline'),
subhead: req.fluentFormat('home-not-found')
})
}
module.exports = {
getEmailMockUps,
notFound,
};
notFound
}

Просмотреть файл

@ -1,106 +1,104 @@
"use strict";
'use strict'
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
const AppConstants = require("../app-constants");
const DB = require("../db/DB");
const EmailUtils = require("../email-utils");
const HIBP = require("../hibp");
const { LocaleUtils } = require ("../locale-utils");
const mozlog = require("../log");
const log = mozlog("controllers.hibp");
const AppConstants = require('../app-constants')
const DB = require('../db/DB')
const EmailUtils = require('../email-utils')
const HIBP = require('../hibp')
const { LocaleUtils } = require('../locale-utils')
const mozlog = require('../log')
const log = mozlog('controllers.hibp')
// Get addresses and language from either subscribers or
// email_addresses fields
function getAddressesAndLanguageForEmail(recipient) {
const preFxaSubscriber = !recipient.fxa_uid ? true : false;
const signupLanguage = recipient.signup_language;
if (recipient.hasOwnProperty("email") && recipient.email) {
function getAddressesAndLanguageForEmail (recipient) {
const preFxaSubscriber = !recipient.fxa_uid
const signupLanguage = recipient.signup_language
if (recipient.hasOwnProperty('email') && recipient.email) {
// email_addresses record, check all_emails_to_primary
if (recipient.all_emails_to_primary) {
return {
recipientEmail: recipient.primary_email,
breachedEmail: recipient.email,
signupLanguage,
preFxaSubscriber,
};
preFxaSubscriber
}
}
return {
recipientEmail: recipient.email,
breachedEmail: recipient.email,
signupLanguage,
preFxaSubscriber,
};
preFxaSubscriber
}
}
return {
recipientEmail: recipient.primary_email,
breachedEmail: recipient.primary_email,
signupLanguage,
preFxaSubscriber,
};
preFxaSubscriber
}
}
async function notify (req, res) {
if (!req.token || req.token !== AppConstants.HIBP_NOTIFY_TOKEN) {
const errorMessage = "HIBP notify endpoint requires valid authorization token.";
throw new Error(errorMessage);
const errorMessage = 'HIBP notify endpoint requires valid authorization token.'
throw new Error(errorMessage)
}
if(!["breachName", "hashPrefix", "hashSuffixes"].every(req.body.hasOwnProperty, req.body)) {
throw new Error("Breach notification requires breachName, hashPrefix, and hashSuffixes.");
if (!['breachName', 'hashPrefix', 'hashSuffixes'].every(req.body.hasOwnProperty, req.body)) {
throw new Error('Breach notification requires breachName, hashPrefix, and hashSuffixes.')
}
const reqBreachName = req.body.breachName.toLowerCase();
const reqHashPrefix = req.body.hashPrefix.toLowerCase();
let breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName);
const reqBreachName = req.body.breachName.toLowerCase()
const reqHashPrefix = req.body.hashPrefix.toLowerCase()
let breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName)
if (!breachAlert) {
// If breach isn't found, try to reload breaches from HIBP
await HIBP.loadBreachesIntoApp(req.app);
breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName);
await HIBP.loadBreachesIntoApp(req.app)
breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName)
if (!breachAlert) {
// If breach *still* isn't found, we have a real error
throw new Error("Unrecognized breach: " + reqBreachName);
throw new Error('Unrecognized breach: ' + reqBreachName)
}
}
if (breachAlert.IsSpamList || breachAlert.IsFabricated || !breachAlert.IsVerified || breachAlert.Domain === "") {
log.info(`${breachAlert.Name} is fabricated, a spam list, not associated with a website, or unverified. \n Breach Alert not sent.`);
if (breachAlert.IsSpamList || breachAlert.IsFabricated || !breachAlert.IsVerified || breachAlert.Domain === '') {
log.info(`${breachAlert.Name} is fabricated, a spam list, not associated with a website, or unverified. \n Breach Alert not sent.`)
return res.status(200).json(
{info: "Breach loaded into database. Subscribers not notified."}
);
{ info: 'Breach loaded into database. Subscribers not notified.' }
)
}
const hashes = req.body.hashSuffixes.map(suffix=>reqHashPrefix + suffix.toLowerCase());
const subscribers = await DB.getSubscribersByHashes(hashes);
const emailAddresses = await DB.getEmailAddressesByHashes(hashes);
const recipients = subscribers.concat(emailAddresses);
log.info("notification", { length: recipients.length, breachAlertName: breachAlert.Name });
const hashes = req.body.hashSuffixes.map(suffix => reqHashPrefix + suffix.toLowerCase())
const subscribers = await DB.getSubscribersByHashes(hashes)
const emailAddresses = await DB.getEmailAddressesByHashes(hashes)
const recipients = subscribers.concat(emailAddresses)
log.info('notification', { length: recipients.length, breachAlertName: breachAlert.Name })
const utmID = "breach-alert";
const notifiedRecipients = [];
const utmID = 'breach-alert'
const notifiedRecipients = []
for (const recipient of recipients) {
log.info("notify", {recipient});
log.info('notify', { recipient })
// Get subscriber ID from "subscriber_id" property (if email_addresses record)
// or from "id" property (if subscribers record)
const subscriberId = recipient.subscriber_id || recipient.id;
const { recipientEmail, breachedEmail, signupLanguage, preFxaSubscriber } = getAddressesAndLanguageForEmail(recipient);
const campaignId = "go-to-dashboard-link";
const ctaHref = EmailUtils.getEmailCtaHref(utmID, campaignId, subscriberId);
const subscriberId = recipient.subscriber_id || recipient.id
const { recipientEmail, breachedEmail, signupLanguage, preFxaSubscriber } = getAddressesAndLanguageForEmail(recipient)
const campaignId = 'go-to-dashboard-link'
const ctaHref = EmailUtils.getEmailCtaHref(utmID, campaignId, subscriberId)
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : "";
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : ''
const supportedLocales = negotiateLanguages(
requestedLanguage,
req.app.locals.AVAILABLE_LANGUAGES,
{defaultLocale: "en"}
);
{ defaultLocale: 'en' }
)
const subject = LocaleUtils.fluentFormat(supportedLocales, "breach-alert-subject");
const template = "default_email";
const subject = LocaleUtils.fluentFormat(supportedLocales, 'breach-alert-subject')
const template = 'default_email'
if (!notifiedRecipients.includes(breachedEmail)) {
await EmailUtils.sendEmail(
recipientEmail, subject, template,
@ -112,30 +110,27 @@ async function notify (req, res) {
breachAlert,
SERVER_URL: AppConstants.SERVER_URL,
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(recipient, utmID),
ctaHref: ctaHref,
whichPartial: "email_partials/report",
preFxaSubscriber,
ctaHref,
whichPartial: 'email_partials/report',
preFxaSubscriber
}
);
notifiedRecipients.push(breachedEmail);
)
notifiedRecipients.push(breachedEmail)
}
}
log.info("notified", { length: notifiedRecipients.length });
res.status(200);
log.info('notified', { length: notifiedRecipients.length })
res.status(200)
res.json(
{info: "Notified subscribers of breach."}
);
{ info: 'Notified subscribers of breach.' }
)
}
async function breaches (req, res, next) {
res.append("Last-Modified", req.app.locals.mostRecentBreachDateTime);
res.json(req.app.locals.breaches);
res.append('Last-Modified', req.app.locals.mostRecentBreachDateTime)
res.json(req.app.locals.breaches)
}
module.exports = {
notify,
breaches,
};
breaches
}

Просмотреть файл

@ -1,160 +1,158 @@
"use strict";
'use strict'
const AppConstants = require("../app-constants");
const DB = require("../db/DB");
const HIBP = require("../hibp");
const { scanResult } = require("../scan-results");
const AppConstants = require('../app-constants')
const DB = require('../db/DB')
const HIBP = require('../hibp')
const { scanResult } = require('../scan-results')
const {
generatePageToken,
getExperimentFlags,
getUTMContents,
hasUserSignedUpForWaitlist,
} = require("./utils");
hasUserSignedUpForWaitlist
} = require('./utils')
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
function _getFeaturedBreach(allBreaches, breachQueryValue) {
function _getFeaturedBreach (allBreaches, breachQueryValue) {
if (!breachQueryValue) {
return null;
return null
}
const lowercaseBreachValue = breachQueryValue.toLowerCase();
return HIBP.getBreachByName(allBreaches, lowercaseBreachValue);
const lowercaseBreachValue = breachQueryValue.toLowerCase()
return HIBP.getBreachByName(allBreaches, lowercaseBreachValue)
}
async function home(req, res) {
async function home (req, res) {
const formTokens = {
pageToken: AppConstants.PAGE_TOKEN_TIMER > 0 ? generatePageToken(req) : "",
csrfToken: req.csrfToken(),
};
pageToken: AppConstants.PAGE_TOKEN_TIMER > 0 ? generatePageToken(req) : '',
csrfToken: req.csrfToken()
}
let featuredBreach = null;
let scanFeaturedBreach = false;
let featuredBreach = null
let scanFeaturedBreach = false
if (req.session.user && !req.query.breach) {
return res.redirect("/user/dashboard");
return res.redirect('/user/dashboard')
}
// Rewrites the /share/{COLOR} links to /
if (req.session.redirectHome) {
req.session.redirectHome = false;
return res.redirect("/");
req.session.redirectHome = false
return res.redirect('/')
}
// Note - If utmOverrides get set, they are unenrolled from the experiment
const utmOverrides = getUTMContents(req);
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
const utmOverrides = getUTMContents(req)
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
if (req.params && req.params.breach) {
req.query.breach = req.params.breach;
req.query.breach = req.params.breach
}
if (req.query.breach) {
featuredBreach = _getFeaturedBreach(req.app.locals.breaches, req.query.breach);
featuredBreach = _getFeaturedBreach(req.app.locals.breaches, req.query.breach)
if (!featuredBreach) {
return notFound(req, res);
return notFound(req, res)
}
const scanRes = await scanResult(req);
const scanRes = await scanResult(req)
if (scanRes.doorhangerScan) {
return res.render("scan", Object.assign(scanRes, formTokens));
return res.render('scan', Object.assign(scanRes, formTokens))
}
scanFeaturedBreach = true;
scanFeaturedBreach = true
return res.render("monitor", {
title: req.fluentFormat("home-title"),
featuredBreach: featuredBreach,
return res.render('monitor', {
title: req.fluentFormat('home-title'),
featuredBreach,
scanFeaturedBreach,
pageToken: formTokens.pageToken,
csrfToken: formTokens.csrfToken,
experimentFlags,
utmOverrides,
});
utmOverrides
})
}
res.render("monitor", {
title: req.fluentFormat("home-title"),
featuredBreach: featuredBreach,
res.render('monitor', {
title: req.fluentFormat('home-title'),
featuredBreach,
scanFeaturedBreach,
pageToken: formTokens.pageToken,
csrfToken: formTokens.csrfToken,
experimentFlags,
utmOverrides,
});
utmOverrides
})
}
function removeMyData(req, res) {
const user = req.user;
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, "remove_data");
return res.render("remove-data", {
title: req.fluentFormat("home-title"),
userHasSignedUpForRemoveData,
});
function removeMyData (req, res) {
const user = req.user
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, 'remove_data')
return res.render('remove-data', {
title: req.fluentFormat('home-title'),
userHasSignedUpForRemoveData
})
}
function getAllBreaches(req, res) {
return res.render("top-level-page", {
title: "Firefox Monitor",
whichPartial: "top-level/all-breaches",
});
function getAllBreaches (req, res) {
return res.render('top-level-page', {
title: 'Firefox Monitor',
whichPartial: 'top-level/all-breaches'
})
}
function getSecurityTips(req, res) {
return res.render("top-level-page", {
title: req.fluentFormat("home-title"),
whichPartial: "top-level/security-tips",
});
function getSecurityTips (req, res) {
return res.render('top-level-page', {
title: req.fluentFormat('home-title'),
whichPartial: 'top-level/security-tips'
})
}
function getAboutPage(req, res) {
return res.render("about",{
title: req.fluentFormat("about-firefox-monitor"),
});
function getAboutPage (req, res) {
return res.render('about', {
title: req.fluentFormat('about-firefox-monitor')
})
}
function getBentoStrings(req, res) {
function getBentoStrings (req, res) {
const localizedBentoStrings = {
bentoButtonTitle: req.fluentFormat("bento-button-title"),
bentoHeadline: req.fluentFormat("fx-makes-tech"),
bentoBottomLink: req.fluentFormat("made-by-mozilla"),
fxDesktop: req.fluentFormat("fx-desktop"),
fxMobile: req.fluentFormat("fx-mobile"),
fxMonitor: req.fluentFormat("fx-monitor"),
pocket: req.fluentFormat("pocket"),
mozVPN: "Mozilla VPN",
mobileCloseBentoButtonTitle: req.fluentFormat("mobile-close-bento-button-title"),
};
return res.json(localizedBentoStrings);
bentoButtonTitle: req.fluentFormat('bento-button-title'),
bentoHeadline: req.fluentFormat('fx-makes-tech'),
bentoBottomLink: req.fluentFormat('made-by-mozilla'),
fxDesktop: req.fluentFormat('fx-desktop'),
fxMobile: req.fluentFormat('fx-mobile'),
fxMonitor: req.fluentFormat('fx-monitor'),
pocket: req.fluentFormat('pocket'),
mozVPN: 'Mozilla VPN',
mobileCloseBentoButtonTitle: req.fluentFormat('mobile-close-bento-button-title')
}
return res.json(localizedBentoStrings)
}
function _addToWaitlistsJoined(user, waitlist) {
function _addToWaitlistsJoined (user, waitlist) {
if (!user.waitlists_joined) {
return {[waitlist]: {"notified": false} };
return { [waitlist]: { notified: false } }
}
user.waitlists_joined[waitlist] = {"notified": false };
return user.waitlists_joined;
user.waitlists_joined[waitlist] = { notified: false }
return user.waitlists_joined
}
function addEmailToWaitlist(req, res) {
function addEmailToWaitlist (req, res) {
if (!req.user) {
return res.redirect("/");
return res.redirect('/')
}
const user = req.user;
const updatedWaitlistsJoined = _addToWaitlistsJoined(user, "remove_data");
DB.setWaitlistsJoined({user, updatedWaitlistsJoined});
return res.json("email-added");
const user = req.user
const updatedWaitlistsJoined = _addToWaitlistsJoined(user, 'remove_data')
DB.setWaitlistsJoined({ user, updatedWaitlistsJoined })
return res.json('email-added')
}
function notFound(req, res) {
res.status(404);
res.render("subpage", {
analyticsID: "error",
headline: req.fluentFormat("error-headline"),
subhead: req.fluentFormat("home-not-found"),
});
function notFound (req, res) {
res.status(404)
res.render('subpage', {
analyticsID: 'error',
headline: req.fluentFormat('error-headline'),
subhead: req.fluentFormat('home-not-found')
})
}
module.exports = {
@ -165,5 +163,5 @@ module.exports = {
getSecurityTips,
home,
notFound,
removeMyData,
};
removeMyData
}

Просмотреть файл

@ -1,40 +1,40 @@
// IP location data includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.
// For testing, you can compare IPs to the corresponding json, eg: https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
"use strict";
'use strict'
const AppConstants = require("../app-constants");
const { readLocationData } = require("../ip-location-service");
const AppConstants = require('../app-constants')
const { readLocationData } = require('../ip-location-service')
async function getIpLocation(req, res) {
let clientIp;
async function getIpLocation (req, res) {
let clientIp
switch(AppConstants.NODE_ENV){
case "dev":
clientIp = req.headers["test-ip"] || "216.160.83.56"; // fallback an IP that exists in limited/local GeoLite2 test DB
break;
case "heroku":
case "stage":
clientIp = req.headers["test-ip"] || req.ip; // Use "test-ip" header if available, fallback to original IP
break;
switch (AppConstants.NODE_ENV) {
case 'dev':
clientIp = req.headers['test-ip'] || '216.160.83.56' // fallback an IP that exists in limited/local GeoLite2 test DB
break
case 'heroku':
case 'stage':
clientIp = req.headers['test-ip'] || req.ip // Use "test-ip" header if available, fallback to original IP
break
default:
clientIp = req.ip;
clientIp = req.ip
}
if (clientIp === req.session.locationData?.clientIp) {
return res.status(200).json(req.session.locationData); // return cached data
return res.status(200).json(req.session.locationData) // return cached data
}
const locationData = await readLocationData(clientIp, req.supportedLocales);
const locationData = await readLocationData(clientIp, req.supportedLocales)
if (locationData) {
req.session.locationData = Object.assign(locationData, { clientIp }); // cache new location data, adding clientIP key
return res.status(200).json(req.session.locationData);
req.session.locationData = Object.assign(locationData, { clientIp }) // cache new location data, adding clientIP key
return res.status(200).json(req.session.locationData)
}
return res.status(200).json({ clientIp });
return res.status(200).json({ clientIp })
}
module.exports = {
getIpLocation,
};
getIpLocation
}

Просмотреть файл

@ -1,107 +1,105 @@
"use strict";
const { URL } = require("url");
'use strict'
const { URL } = require('url')
const crypto = require("crypto");
const crypto = require('crypto')
const AppConstants = require("../app-constants");
const DB = require("../db/DB");
const EmailUtils = require("../email-utils");
const {FXA, FxAOAuthClient} = require("../lib/fxa");
const { FluentError } = require("../locale-utils");
const HIBP = require("../hibp");
const mozlog = require("../log");
const sha1 = require("../sha1-utils");
const AppConstants = require('../app-constants')
const DB = require('../db/DB')
const EmailUtils = require('../email-utils')
const { FXA, FxAOAuthClient } = require('../lib/fxa')
const { FluentError } = require('../locale-utils')
const HIBP = require('../hibp')
const mozlog = require('../log')
const sha1 = require('../sha1-utils')
const log = mozlog("controllers.oauth");
const log = mozlog('controllers.oauth')
function init(req, res, next, client = FxAOAuthClient) {
function init (req, res, next, client = FxAOAuthClient) {
// Set a random state string in a cookie so that we can verify
// the user when they're redirected back to us after auth.
const state = crypto.randomBytes(40).toString("hex");
req.session.state = state;
const url = new URL(client.code.getUri({state}));
const fxaParams = new URL(req.url, AppConstants.SERVER_URL);
const state = crypto.randomBytes(40).toString('hex')
req.session.state = state
const url = new URL(client.code.getUri({ state }))
const fxaParams = new URL(req.url, AppConstants.SERVER_URL)
req.session.utmContents = {};
req.session.utmContents = {}
url.searchParams.append("access_type", "offline");
url.searchParams.append("action", "email");
url.searchParams.append('access_type', 'offline')
url.searchParams.append('action', 'email')
for (const param of fxaParams.searchParams.keys()) {
url.searchParams.append(param, fxaParams.searchParams.get(param));
url.searchParams.append(param, fxaParams.searchParams.get(param))
}
res.redirect(url);
res.redirect(url)
}
async function confirmed(req, res, next, client = FxAOAuthClient) {
async function confirmed (req, res, next, client = FxAOAuthClient) {
if (!req.session.state) {
throw new FluentError("oauth-invalid-session");
throw new FluentError('oauth-invalid-session')
}
if (req.session.state !== req.query.state) {
throw new FluentError("oauth-invalid-session");
throw new FluentError('oauth-invalid-session')
}
const fxaUser = await client.code.getToken(req.originalUrl, { state: req.session.state });
const fxaUser = await client.code.getToken(req.originalUrl, { state: req.session.state })
// Clear the session.state to clean up and avoid any replays
req.session.state = null;
log.debug("fxa-confirmed-fxaUser", fxaUser);
const fxaProfileData = await FXA.getProfileData(fxaUser.accessToken);
log.debug("fxa-confirmed-profile-data", fxaProfileData);
const email = JSON.parse(fxaProfileData).email;
req.session.state = null
log.debug('fxa-confirmed-fxaUser', fxaUser)
const fxaProfileData = await FXA.getProfileData(fxaUser.accessToken)
log.debug('fxa-confirmed-profile-data', fxaProfileData)
const email = JSON.parse(fxaProfileData).email
const existingUser = await DB.getSubscriberByEmail(email);
req.session.user = existingUser;
const existingUser = await DB.getSubscriberByEmail(email)
req.session.user = existingUser
const returnURL = new URL("/user/dashboard", AppConstants.SERVER_URL);
const returnURL = new URL('/user/dashboard', AppConstants.SERVER_URL)
// Check if user is signing up or signing in,
// then add new users to db and send email.
if (!existingUser || existingUser.fxa_refresh_token === null) {
// req.session.newUser determines whether or not we show "fxa_new_user_bar" in template
req.session.newUser = true;
const signupLanguage = req.headers["accept-language"];
const verifiedSubscriber = await DB.addSubscriber(email, signupLanguage, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData);
req.session.newUser = true
const signupLanguage = req.headers['accept-language']
const verifiedSubscriber = await DB.addSubscriber(email, signupLanguage, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData)
// duping some of user/verify for now
let unsafeBreachesForEmail = [];
let unsafeBreachesForEmail = []
unsafeBreachesForEmail = await HIBP.getBreachesForEmail(
sha1(email),
req.app.locals.breaches,
true
);
const utmID = "report";
const reportSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req);
)
const utmID = 'report'
const reportSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req)
await EmailUtils.sendEmail(
email,
reportSubject,
"default_email",
'default_email',
{
supportedLocales: req.supportedLocales,
breachedEmail: email,
recipientEmail: email,
date: req.fluentFormat(new Date()),
unsafeBreachesForEmail: unsafeBreachesForEmail,
ctaHref: EmailUtils.getEmailCtaHref(utmID, "go-to-dashboard-link"),
unsafeBreachesForEmail,
ctaHref: EmailUtils.getEmailCtaHref(utmID, 'go-to-dashboard-link'),
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(verifiedSubscriber, utmID),
whichPartial: "email_partials/report",
whichPartial: 'email_partials/report'
}
);
req.session.user = verifiedSubscriber;
return res.redirect(returnURL.pathname + returnURL.search);
)
req.session.user = verifiedSubscriber
return res.redirect(returnURL.pathname + returnURL.search)
}
// Update existing user's FxA data
await DB._updateFxAData(existingUser, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData);
res.redirect(returnURL.pathname + returnURL.search);
await DB._updateFxAData(existingUser, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData)
res.redirect(returnURL.pathname + returnURL.search)
}
module.exports = {
init,
confirmed,
};
confirmed
}

Просмотреть файл

@ -1,49 +1,48 @@
"use strict";
'use strict'
const crypto = require("crypto");
const crypto = require('crypto')
const AppConstants = require("../app-constants");
const { FluentError } = require("../locale-utils");
const AppConstants = require('../app-constants')
const { FluentError } = require('../locale-utils')
// const { generatePageToken } = require("./utils");
const mozlog = require("../log");
const { scanResult } = require("../scan-results");
const sha1 = require("../sha1-utils");
const mozlog = require('../log')
const { scanResult } = require('../scan-results')
const sha1 = require('../sha1-utils')
const log = mozlog("controllers.scan");
const log = mozlog('controllers.scan')
function _decryptPageToken(encryptedPageToken) {
let decipher;
function _decryptPageToken (encryptedPageToken) {
let decipher
if (encryptedPageToken.slice(24, 25) === ".") {
if (encryptedPageToken.slice(24, 25) === '.') {
// iv becomes 24 characters long when represented as base64: ceil(16 / 3) * 4 = 24
const iv = Buffer.from(encryptedPageToken.slice(0, 24), "base64");
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, "sha512");
const iv = Buffer.from(encryptedPageToken.slice(0, 24), 'base64')
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, 'sha512')
decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
encryptedPageToken = encryptedPageToken.slice(25);
decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
encryptedPageToken = encryptedPageToken.slice(25)
} else {
decipher = crypto.createDecipher("aes-256-cbc", AppConstants.COOKIE_SECRET);
decipher = crypto.createDecipher('aes-256-cbc', AppConstants.COOKIE_SECRET)
}
const decryptedPageToken = Buffer.concat([
decipher.update(Buffer.from(encryptedPageToken, "base64")),
decipher.final(),
]).toString("utf8");
decipher.update(Buffer.from(encryptedPageToken, 'base64')),
decipher.final()
]).toString('utf8')
return JSON.parse(decryptedPageToken);
return JSON.parse(decryptedPageToken)
}
function _validatePageToken(pageToken, req) {
const requestIP = req.headers["x-real-ip"] || req.ip;
const pageTokenIP = pageToken.ip;
function _validatePageToken (pageToken, req) {
const requestIP = req.headers['x-real-ip'] || req.ip
const pageTokenIP = pageToken.ip
if (pageToken.ip !== requestIP) {
log.error("_validatePageToken", {msg: "IP mis-match", pageTokenIP, requestIP});
return false;
log.error('_validatePageToken', { msg: 'IP mis-match', pageTokenIP, requestIP })
return false
}
if (Date.now() - new Date(pageToken.date) >= AppConstants.PAGE_TOKEN_TIMER * 1000) {
log.error("_validatePageToken", {msg: "expired pageToken"});
return false;
log.error('_validatePageToken', { msg: 'expired pageToken' })
return false
}
/* TODO: block on scans-per-ip instead of scans-per-timespan
if (req.session.scans.length > 5) {
@ -55,52 +54,49 @@ function _validatePageToken(pageToken, req) {
req.session.scans.push(emailHash);
}
*/
return pageToken;
return pageToken
}
async function post (req, res) {
const emailHash = req.body.emailHash;
const encryptedPageToken = req.body.pageToken;
let validPageToken = false;
const emailHash = req.body.emailHash
const encryptedPageToken = req.body.pageToken
let validPageToken = false
// for #688: use a page token to check that scans come from real pages
if (AppConstants.PAGE_TOKEN_TIMER > 0) {
if (!encryptedPageToken) {
throw new FluentError("error-scan-page-token");
throw new FluentError('error-scan-page-token')
}
const decryptedPageToken = _decryptPageToken(encryptedPageToken);
validPageToken = _validatePageToken(decryptedPageToken, req);
const decryptedPageToken = _decryptPageToken(encryptedPageToken)
validPageToken = _validatePageToken(decryptedPageToken, req)
if (!validPageToken) {
throw new FluentError("error-scan-page-token");
throw new FluentError('error-scan-page-token')
}
}
if (!emailHash || emailHash === sha1("")) {
return res.redirect("/");
if (!emailHash || emailHash === sha1('')) {
return res.redirect('/')
}
const scanRes = await scanResult(req);
const scanRes = await scanResult(req)
const formTokens = {
pageToken: encryptedPageToken,
csrfToken: req.csrfToken(),
};
csrfToken: req.csrfToken()
}
if (req.session.user && scanRes.selfScan && !req.body.featuredBreach) {
return res.redirect("/user/dashboard");
return res.redirect('/user/dashboard')
}
res.render("scan", Object.assign(scanRes, formTokens));
res.render('scan', Object.assign(scanRes, formTokens))
}
function get (req, res) {
res.redirect("/");
res.redirect('/')
}
module.exports = {
post,
get,
};
get
}

Просмотреть файл

@ -1,97 +1,88 @@
"use strict";
'use strict'
const MessageValidator = require("sns-validator");
const MessageValidator = require('sns-validator')
const DB = require("../db/DB");
const DB = require('../db/DB')
const mozlog = require("../log");
const mozlog = require('../log')
const validator = new MessageValidator()
const log = mozlog('controllers.ses')
const validator = new MessageValidator();
const log = mozlog("controllers.ses");
async function notification(req, res) {
const message = JSON.parse(req.body);
async function notification (req, res) {
const message = JSON.parse(req.body)
return new Promise((resolve, reject) => {
validator.validate(message, async (err, message) => {
if (err) {
log.error("notification", { err: err });
const body = "Access denied. " + err.message;
res.status(401).send(body);
return reject(body);
log.error('notification', { err })
const body = 'Access denied. ' + err.message
res.status(401).send(body)
return reject(body)
}
await handleNotification(message);
await handleNotification(message)
res.status(200).json(
{status: "OK"}
);
return resolve("OK");
});
});
{ status: 'OK' }
)
return resolve('OK')
})
})
}
async function handleNotification(notification) {
log.info("received-SNS", { id: notification.MessageId });
const message = JSON.parse(notification.Message);
if (message.hasOwnProperty("eventType")) {
await handleSESMessage(message);
async function handleNotification (notification) {
log.info('received-SNS', { id: notification.MessageId })
const message = JSON.parse(notification.Message)
if (message.hasOwnProperty('eventType')) {
await handleSESMessage(message)
}
if (message.hasOwnProperty("event")) {
await handleFxAMessage(message);
if (message.hasOwnProperty('event')) {
await handleFxAMessage(message)
}
}
async function handleFxAMessage(message) {
async function handleFxAMessage (message) {
switch (message.event) {
case "delete":
await handleDeleteMessage(message);
break;
case 'delete':
await handleDeleteMessage(message)
break
default:
log.info("unhandled-event", { event: message.event });
log.info('unhandled-event', { event: message.event })
}
}
async function handleDeleteMessage(message) {
await DB.deleteSubscriberByFxAUID(message.uid);
async function handleDeleteMessage (message) {
await DB.deleteSubscriberByFxAUID(message.uid)
}
async function handleSESMessage(message) {
async function handleSESMessage (message) {
switch (message.eventType) {
case "Bounce":
await handleBounceMessage(message);
break;
case "Complaint":
await handleComplaintMessage(message);
break;
case 'Bounce':
await handleBounceMessage(message)
break
case 'Complaint':
await handleComplaintMessage(message)
break
default:
log.info("unhandled-eventType", { type: message.eventType });
log.info('unhandled-eventType', { type: message.eventType })
}
}
async function handleBounceMessage(message) {
const bounce = message.bounce;
if (bounce.bounceType === "Permanent") {
return await removeSubscribersFromDB(bounce.bouncedRecipients);
async function handleBounceMessage (message) {
const bounce = message.bounce
if (bounce.bounceType === 'Permanent') {
return await removeSubscribersFromDB(bounce.bouncedRecipients)
}
}
async function handleComplaintMessage(message) {
const complaint = message.complaint;
return await removeSubscribersFromDB(complaint.complainedRecipients);
async function handleComplaintMessage (message) {
const complaint = message.complaint
return await removeSubscribersFromDB(complaint.complainedRecipients)
}
async function removeSubscribersFromDB(recipients) {
async function removeSubscribersFromDB (recipients) {
for (const recipient of recipients) {
await DB.removeEmail(recipient.emailAddress);
await DB.removeEmail(recipient.emailAddress)
}
}
@ -100,5 +91,5 @@ module.exports = {
handleNotification,
handleBounceMessage,
handleComplaintMessage,
removeSubscribersFromDB,
};
removeSubscribersFromDB
}

Просмотреть файл

@ -1,534 +1,519 @@
"use strict";
'use strict'
const AppConstants = require("../app-constants");
const isemail = require("isemail");
const AppConstants = require('../app-constants')
const isemail = require('isemail')
const DB = require("../db/DB");
const EmailUtils = require("../email-utils");
const { FluentError } = require("../locale-utils");
const { FXA } = require("../lib/fxa");
const HIBP = require("../hibp");
const { resultsSummary } = require("../scan-results");
const sha1 = require("../sha1-utils");
const DB = require('../db/DB')
const EmailUtils = require('../email-utils')
const { FluentError } = require('../locale-utils')
const { FXA } = require('../lib/fxa')
const HIBP = require('../hibp')
const { resultsSummary } = require('../scan-results')
const sha1 = require('../sha1-utils')
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
const {
getExperimentFlags,
getUTMContents,
hasUserSignedUpForWaitlist,
} = require("./utils");
hasUserSignedUpForWaitlist
} = require('./utils')
const FXA_MONITOR_SCOPE = "https://identity.mozilla.com/apps/monitor";
const FXA_MONITOR_SCOPE = 'https://identity.mozilla.com/apps/monitor'
async function removeEmail(req, res) {
const emailId = req.body.emailId;
const sessionUser = req.user;
const existingEmail = await DB.getEmailById(emailId);
async function removeEmail (req, res) {
const emailId = req.body.emailId
const sessionUser = req.user
const existingEmail = await DB.getEmailById(emailId)
if (existingEmail.subscriber_id !== sessionUser.id) {
throw new FluentError("error-not-subscribed");
throw new FluentError('error-not-subscribed')
}
DB.removeOneSecondaryEmail(emailId);
res.redirect("/user/preferences");
DB.removeOneSecondaryEmail(emailId)
res.redirect('/user/preferences')
}
async function resendEmail(req, res) {
const emailId = req.body.emailId;
const sessionUser = req.user;
const existingEmail = await DB.getEmailById(emailId);
async function resendEmail (req, res) {
const emailId = req.body.emailId
const sessionUser = req.user
const existingEmail = await DB.getEmailById(emailId)
if (!existingEmail || !existingEmail.subscriber_id) {
throw new FluentError("user-verify-token-error");
throw new FluentError('user-verify-token-error')
}
if (existingEmail.subscriber_id !== sessionUser.id) {
// TODO: more specific error message?
throw new FluentError("user-verify-token-error");
throw new FluentError('user-verify-token-error')
}
const unverifiedEmailAddressRecord = await DB.resetUnverifiedEmailAddress(emailId);
const unverifiedEmailAddressRecord = await DB.resetUnverifiedEmailAddress(emailId)
const email = unverifiedEmailAddressRecord.email;
const email = unverifiedEmailAddressRecord.email
await EmailUtils.sendEmail(
email,
req.fluentFormat("email-subject-verify"),
"default_email",
{ recipientEmail: email,
req.fluentFormat('email-subject-verify'),
'default_email',
{
recipientEmail: email,
supportedLocales: req.supportedLocales,
ctaHref: EmailUtils.getVerificationUrl(unverifiedEmailAddressRecord),
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedEmailAddressRecord, "account-verification-email"),
whichPartial: "email_partials/email_verify",
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedEmailAddressRecord, 'account-verification-email'),
whichPartial: 'email_partials/email_verify'
}
);
)
// TODO: what should we return to the client?
return res.json("Resent the email");
return res.json('Resent the email')
}
async function updateCommunicationOptions(req, res) {
const sessionUser = req.user;
async function updateCommunicationOptions (req, res) {
const sessionUser = req.user
// 0 = Send breach alerts to the email address found in brew breach.
// 1 = Send all breach alerts to user's primary email address.
const allEmailsToPrimary = (Number(req.body.communicationOption) === 1) ? true : false;
const updatedSubscriber = await DB.setAllEmailsToPrimary(sessionUser, allEmailsToPrimary);
req.session.user = updatedSubscriber;
const allEmailsToPrimary = (Number(req.body.communicationOption) === 1)
const updatedSubscriber = await DB.setAllEmailsToPrimary(sessionUser, allEmailsToPrimary)
req.session.user = updatedSubscriber
return res.json("Comm options updated");
return res.json('Comm options updated')
}
async function resolveBreach(req, res) {
const sessionUser = req.user;
async function resolveBreach (req, res) {
const sessionUser = req.user
// TODO: verify that req.body.emailAddressId belongs to sessionUser
const updatedSubscriber = await DB.setResolvedBreach({
subscriber: sessionUser,
emailAddresses: req.body.emailAddressId,
recencyIndex: req.body.recencyIndex,
});
req.session.user = updatedSubscriber;
return res.json("Breach marked as resolved.");
recencyIndex: req.body.recencyIndex
})
req.session.user = updatedSubscriber
return res.json('Breach marked as resolved.')
}
function _checkForDuplicateEmail(sessionUser, email) {
email = email.toLowerCase();
function _checkForDuplicateEmail (sessionUser, email) {
email = email.toLowerCase()
if (email === sessionUser.primary_email.toLowerCase()) {
throw new FluentError("user-add-duplicate-email");
throw new FluentError('user-add-duplicate-email')
}
for (const secondaryEmail of sessionUser.email_addresses) {
if (email === secondaryEmail.email.toLowerCase()) {
throw new FluentError("user-add-duplicate-email");
throw new FluentError('user-add-duplicate-email')
}
}
}
async function add(req, res) {
const sessionUser = await req.user;
const email = req.body.email;
async function add (req, res) {
const sessionUser = await req.user
const email = req.body.email
if (!email || !isemail.validate(email)) {
throw new FluentError("user-add-invalid-email");
throw new FluentError('user-add-invalid-email')
}
if (sessionUser.email_addresses.length >= AppConstants.MAX_NUM_ADDRESSES) {
throw new FluentError("user-add-too-many-emails");
throw new FluentError('user-add-too-many-emails')
}
_checkForDuplicateEmail(sessionUser, email);
_checkForDuplicateEmail(sessionUser, email)
const unverifiedSubscriber = await DB.addSubscriberUnverifiedEmailHash(
req.session.user, email
);
)
await EmailUtils.sendEmail(
email,
req.fluentFormat("email-subject-verify"),
"default_email",
{ breachedEmail: email,
req.fluentFormat('email-subject-verify'),
'default_email',
{
breachedEmail: email,
recipientEmail: email,
supportedLocales: req.supportedLocales,
ctaHref: EmailUtils.getVerificationUrl(unverifiedSubscriber),
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedSubscriber, "account-verification-email"),
whichPartial: "email_partials/email_verify",
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedSubscriber, 'account-verification-email'),
whichPartial: 'email_partials/email_verify'
}
);
)
// send users coming from the dashboard back to the dashboard
// and set req.session.lastAddedEmail to show a confirmation message.
if (req.headers.referer.endsWith("/dashboard")) {
req.session.lastAddedEmail = email;
return res.redirect("/user/dashboard");
if (req.headers.referer.endsWith('/dashboard')) {
req.session.lastAddedEmail = email
return res.redirect('/user/dashboard')
}
res.redirect("/user/preferences");
res.redirect('/user/preferences')
}
function getResolvedBreachesForEmail(user, email) {
function getResolvedBreachesForEmail (user, email) {
if (user.breaches_resolved === null) {
return [];
return []
}
return user.breaches_resolved.hasOwnProperty(email) ? user.breaches_resolved[email] : [];
return user.breaches_resolved.hasOwnProperty(email) ? user.breaches_resolved[email] : []
}
function addResolvedOrNot(foundBreaches, resolvedBreaches) {
const annotatedBreaches = [];
if (AppConstants.BREACH_RESOLUTION_ENABLED !== "1") {
return foundBreaches;
function addResolvedOrNot (foundBreaches, resolvedBreaches) {
const annotatedBreaches = []
if (AppConstants.BREACH_RESOLUTION_ENABLED !== '1') {
return foundBreaches
}
for (const breach of foundBreaches) {
const IsResolved = resolvedBreaches.includes(breach.recencyIndex) ? true : false;
annotatedBreaches.push(Object.assign({IsResolved}, breach));
const IsResolved = !!resolvedBreaches.includes(breach.recencyIndex)
annotatedBreaches.push(Object.assign({ IsResolved }, breach))
}
return annotatedBreaches;
return annotatedBreaches
}
function addRecencyIndex(foundBreaches) {
const annotatedBreaches = [];
function addRecencyIndex (foundBreaches) {
const annotatedBreaches = []
// slice() the array to make a copy so before reversing so we don't
// reverse foundBreaches in-place
const oldestToNewestFoundBreaches = foundBreaches.slice().reverse();
oldestToNewestFoundBreaches.forEach( (annotatingBreach, index) => {
const foundBreach = foundBreaches.find( foundBreach => foundBreach.Name === annotatingBreach.Name);
annotatedBreaches.push(Object.assign({recencyIndex: index}, foundBreach));
});
return annotatedBreaches.reverse();
const oldestToNewestFoundBreaches = foundBreaches.slice().reverse()
oldestToNewestFoundBreaches.forEach((annotatingBreach, index) => {
const foundBreach = foundBreaches.find(foundBreach => foundBreach.Name === annotatingBreach.Name)
annotatedBreaches.push(Object.assign({ recencyIndex: index }, foundBreach))
})
return annotatedBreaches.reverse()
}
async function bundleVerifiedEmails(options) {
const { user, email, recordId, recordVerified, allBreaches} = options;
const lowerCaseEmailSha = sha1(email.toLowerCase());
const foundBreaches = await HIBP.getBreachesForEmail(lowerCaseEmailSha, allBreaches, true, false);
const foundBreachesWithRecency = addRecencyIndex(foundBreaches);
const resolvedBreaches = getResolvedBreachesForEmail(user, email);
const foundBreachesWithResolutions = addResolvedOrNot(foundBreachesWithRecency, resolvedBreaches);
const filteredAnnotatedFoundBreaches = HIBP.filterBreaches(foundBreachesWithResolutions);
async function bundleVerifiedEmails (options) {
const { user, email, recordId, recordVerified, allBreaches } = options
const lowerCaseEmailSha = sha1(email.toLowerCase())
const foundBreaches = await HIBP.getBreachesForEmail(lowerCaseEmailSha, allBreaches, true, false)
const foundBreachesWithRecency = addRecencyIndex(foundBreaches)
const resolvedBreaches = getResolvedBreachesForEmail(user, email)
const foundBreachesWithResolutions = addResolvedOrNot(foundBreachesWithRecency, resolvedBreaches)
const filteredAnnotatedFoundBreaches = HIBP.filterBreaches(foundBreachesWithResolutions)
const emailEntry = {
"email": email,
"breaches": filteredAnnotatedFoundBreaches,
"primary": email === user.primary_email,
"id": recordId,
"verified": recordVerified,
};
email,
breaches: filteredAnnotatedFoundBreaches,
primary: email === user.primary_email,
id: recordId,
verified: recordVerified
}
return emailEntry;
return emailEntry
}
async function getAllEmailsAndBreaches(user, allBreaches) {
const monitoredEmails = await DB.getUserEmails(user.id);
let verifiedEmails = [];
const unverifiedEmails = [];
verifiedEmails.push(await bundleVerifiedEmails({user, email: user.primary_email, recordId: user.id, recordVerified: user.primary_verified, allBreaches}));
async function getAllEmailsAndBreaches (user, allBreaches) {
const monitoredEmails = await DB.getUserEmails(user.id)
let verifiedEmails = []
const unverifiedEmails = []
verifiedEmails.push(await bundleVerifiedEmails({ user, email: user.primary_email, recordId: user.id, recordVerified: user.primary_verified, allBreaches }))
for (const email of monitoredEmails) {
if (email.verified) {
verifiedEmails.push(await bundleVerifiedEmails({user, email: email.email, recordId: email.id, recordVerified: email.verified, allBreaches}));
verifiedEmails.push(await bundleVerifiedEmails({ user, email: email.email, recordId: email.id, recordVerified: email.verified, allBreaches }))
} else {
unverifiedEmails.push(email);
unverifiedEmails.push(email)
}
}
verifiedEmails = getNewBreachesForEmailEntriesSinceDate(verifiedEmails, user.breaches_last_shown);
return { verifiedEmails, unverifiedEmails };
verifiedEmails = getNewBreachesForEmailEntriesSinceDate(verifiedEmails, user.breaches_last_shown)
return { verifiedEmails, unverifiedEmails }
}
function getNewBreachesForEmailEntriesSinceDate(emailEntries, date) {
function getNewBreachesForEmailEntriesSinceDate (emailEntries, date) {
for (const emailEntry of emailEntries) {
const newBreachesForEmail = emailEntry.breaches.filter(breach => breach.AddedDate >= date);
const newBreachesForEmail = emailEntry.breaches.filter(breach => breach.AddedDate >= date)
for (const newBreachForEmail of newBreachesForEmail) {
newBreachForEmail["NewBreach"] = true; // add "NewBreach" property to the new breach.
emailEntry["hasNewBreaches"] = newBreachesForEmail.length; // add the number of new breaches to the email
newBreachForEmail.NewBreach = true // add "NewBreach" property to the new breach.
emailEntry.hasNewBreaches = newBreachesForEmail.length // add the number of new breaches to the email
}
}
return emailEntries;
return emailEntries
}
async function getDashboard (req, res) {
const user = req.user
const allBreaches = req.app.locals.breaches
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
const utmOverrides = getUTMContents(req)
const supportedLocalesIncludesEnglish = req.supportedLocales.includes('en')
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, 'remove_data')
async function getDashboard(req, res) {
const user = req.user;
const allBreaches = req.app.locals.breaches;
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
const utmOverrides = getUTMContents(req);
const supportedLocalesIncludesEnglish = req.supportedLocales.includes("en");
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, "remove_data");
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
let lastAddedEmail = null
let lastAddedEmail = null;
req.session.user = await DB.setBreachesLastShownNow(user);
req.session.user = await DB.setBreachesLastShownNow(user)
if (req.session.lastAddedEmail) {
lastAddedEmail = req.session.lastAddedEmail;
req.session["lastAddedEmail"] = null;
lastAddedEmail = req.session.lastAddedEmail
req.session.lastAddedEmail = null
}
res.render("dashboards", {
title: req.fluentFormat("Firefox Monitor"),
res.render('dashboards', {
title: req.fluentFormat('Firefox Monitor'),
csrfToken: req.csrfToken(),
lastAddedEmail,
verifiedEmails,
unverifiedEmails,
userHasSignedUpForRemoveData,
supportedLocalesIncludesEnglish,
whichPartial: "dashboards/breaches-dash",
whichPartial: 'dashboards/breaches-dash',
experimentFlags,
utmOverrides,
});
utmOverrides
})
}
async function _verify(req) {
const verifiedEmailHash = await DB.verifyEmailHash(req.query.token);
let unsafeBreachesForEmail = [];
async function _verify (req) {
const verifiedEmailHash = await DB.verifyEmailHash(req.query.token)
let unsafeBreachesForEmail = []
unsafeBreachesForEmail = await HIBP.getBreachesForEmail(
sha1(verifiedEmailHash.email.toLowerCase()),
req.app.locals.breaches,
true
);
const utmID = "report";
const emailSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req);
)
const utmID = 'report'
const emailSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req)
await EmailUtils.sendEmail(
verifiedEmailHash.email,
emailSubject,
"default_email",
'default_email',
{
breachedEmail: verifiedEmailHash.email,
recipientEmail: verifiedEmailHash.email,
supportedLocales: req.supportedLocales,
unsafeBreachesForEmail: unsafeBreachesForEmail,
ctaHref: EmailUtils.getEmailCtaHref(utmID, "go-to-dashboard-link"),
unsafeBreachesForEmail,
ctaHref: EmailUtils.getEmailCtaHref(utmID, 'go-to-dashboard-link'),
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(verifiedEmailHash, utmID),
whichPartial: "email_partials/report",
whichPartial: 'email_partials/report'
}
);
)
}
async function verify(req, res) {
async function verify (req, res) {
if (!req.query.token) {
throw new FluentError("user-verify-token-error");
throw new FluentError('user-verify-token-error')
}
const existingEmail = await DB.getEmailByToken(req.query.token);
const existingEmail = await DB.getEmailByToken(req.query.token)
if (!existingEmail) {
throw new FluentError("error-not-subscribed");
throw new FluentError('error-not-subscribed')
}
const sessionUser = await req.user;
const sessionUser = await req.user
if (sessionUser && existingEmail.subscriber_id !== sessionUser.id) {
// TODO: more specific error message?
// e.g., "This email verification token is not valid for this account"
throw new FluentError("user-verify-token-error");
throw new FluentError('user-verify-token-error')
}
if (!existingEmail.verified) {
await _verify(req);
await _verify(req)
}
if (sessionUser) {
res.redirect("/user/dashboard");
return;
res.redirect('/user/dashboard')
return
}
res.render("subpage", {
title: "Email Verified",
whichPartial: "subpages/confirm",
email: existingEmail.email,
});
res.render('subpage', {
title: 'Email Verified',
whichPartial: 'subpages/confirm',
email: existingEmail.email
})
}
// legacy /user/unsubscribe controller for pre-FxA unsubscribe links
async function getUnsubscribe(req, res) {
async function getUnsubscribe (req, res) {
if (!req.query.token) {
throw new FluentError("user-unsubscribe-token-error");
throw new FluentError('user-unsubscribe-token-error')
}
const subscriber = await DB.getSubscriberByToken(req.query.token);
const subscriber = await DB.getSubscriberByToken(req.query.token)
if (subscriber && subscriber.fxa_profile_json !== null) {
// Token is for a primary email address of an FxA subscriber:
// redirect to preferences to remove Firefox Monitor
return res.redirect("/user/preferences");
return res.redirect('/user/preferences')
}
const emailAddress = await DB.getEmailByToken(req.query.token);
const emailAddress = await DB.getEmailByToken(req.query.token)
if (!subscriber && !emailAddress) {
// Unknown token:
// throw error
throw new FluentError("error-not-subscribed");
throw new FluentError('error-not-subscribed')
}
// Token is for an old pre-FxA subscriber, or a secondary email address:
// render the unsubscribe page
res.render("subpage", {
title: req.fluentFormat("user-unsubscribe-title"),
whichPartial: "subpages/unsubscribe",
res.render('subpage', {
title: req.fluentFormat('user-unsubscribe-title'),
whichPartial: 'subpages/unsubscribe',
token: req.query.token,
hash: req.query.hash,
});
hash: req.query.hash
})
}
async function getRemoveFxm (req, res) {
const sessionUser = req.user
async function getRemoveFxm(req, res) {
const sessionUser = req.user;
res.render("subpage", {
title: req.fluentFormat("remove-fxm"),
res.render('subpage', {
title: req.fluentFormat('remove-fxm'),
subscriber: sessionUser,
whichPartial: "subpages/remove_fxm",
csrfToken: req.csrfToken(),
});
whichPartial: 'subpages/remove_fxm',
csrfToken: req.csrfToken()
})
}
async function postRemoveFxm (req, res) {
const sessionUser = req.user
await DB.removeSubscriber(sessionUser)
await FXA.revokeOAuthTokens(sessionUser)
async function postRemoveFxm(req, res) {
const sessionUser = req.user;
await DB.removeSubscriber(sessionUser);
await FXA.revokeOAuthTokens(sessionUser);
req.session.destroy();
res.redirect("/");
req.session.destroy()
res.redirect('/')
}
function _updateResolvedBreaches(options) {
function _updateResolvedBreaches (options) {
const {
user,
affectedEmail,
isResolved,
recencyIndexNumber,
} = options;
recencyIndexNumber
} = options
// TODO: clarify the logic here. maybe change the endpoint to PUT /breach-resolution
// with the new resolution value ?
const userBreachesResolved = user.breaches_resolved === null ? {} : user.breaches_resolved;
if (isResolved === "false") {
const userBreachesResolved = user.breaches_resolved === null ? {} : user.breaches_resolved
if (isResolved === 'false') {
if (Array.isArray(userBreachesResolved[affectedEmail])) {
userBreachesResolved[affectedEmail].push(recencyIndexNumber);
return userBreachesResolved;
userBreachesResolved[affectedEmail].push(recencyIndexNumber)
return userBreachesResolved
}
userBreachesResolved[affectedEmail] = [recencyIndexNumber];
return userBreachesResolved;
userBreachesResolved[affectedEmail] = [recencyIndexNumber]
return userBreachesResolved
}
userBreachesResolved[affectedEmail] = userBreachesResolved[affectedEmail].filter( el => el !== recencyIndexNumber );
return userBreachesResolved;
userBreachesResolved[affectedEmail] = userBreachesResolved[affectedEmail].filter(el => el !== recencyIndexNumber)
return userBreachesResolved
}
async function postResolveBreach(req, res) {
const sessionUser = req.user;
const { affectedEmail, recencyIndex, isResolved } = req.body;
const recencyIndexNumber = Number(recencyIndex);
const affectedEmailIsSubscriberRecord = sessionUser.primary_email === affectedEmail;
const affectedEmailInEmailAddresses = sessionUser.email_addresses.filter( ea => {
ea.email === affectedEmail;
});
async function postResolveBreach (req, res) {
const sessionUser = req.user
const { affectedEmail, recencyIndex, isResolved } = req.body
const recencyIndexNumber = Number(recencyIndex)
const affectedEmailIsSubscriberRecord = sessionUser.primary_email === affectedEmail
const affectedEmailInEmailAddresses = sessionUser.email_addresses.filter(ea => {
ea.email === affectedEmail
})
if (!affectedEmailIsSubscriberRecord && !affectedEmailInEmailAddresses) {
return res.json("Error: affectedEmail is not valid for this subscriber");
return res.json('Error: affectedEmail is not valid for this subscriber')
}
const updatedResolvedBreaches = _updateResolvedBreaches({
user: sessionUser,
affectedEmail,
isResolved,
recencyIndexNumber,
});
recencyIndexNumber
})
const updatedSubscriber = await DB.setBreachesResolved(
{ user: sessionUser, updatedResolvedBreaches }
);
req.session.user = updatedSubscriber;
)
req.session.user = updatedSubscriber
// return res.json("Breach marked as resolved.");
// Currently we're sending { affectedEmail, recencyIndex, isResolved, passwordsExposed } in req.body
// Not sure if we need all of these or need to send other/additional values?
if (isResolved === "true") {
if (isResolved === 'true') {
// the user clicked "Undo" so mark the breach as unresolved
return res.redirect("/");
return res.redirect('/')
}
const allBreaches = req.app.locals.breaches;
const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches);
const allBreaches = req.app.locals.breaches
const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches)
const userBreachStats = resultsSummary(verifiedEmails);
const numTotalBreaches = userBreachStats.numBreaches.count;
const numResolvedBreaches = userBreachStats.numBreaches.numResolved;
const userBreachStats = resultsSummary(verifiedEmails)
const numTotalBreaches = userBreachStats.numBreaches.count
const numResolvedBreaches = userBreachStats.numBreaches.numResolved
const localizedModalStrings = {
headline: "",
progressMessage: "",
progressStatus: req.fluentFormat( "progress-status", {
numResolvedBreaches: numResolvedBreaches,
numTotalBreaches: numTotalBreaches }
headline: '',
progressMessage: '',
progressStatus: req.fluentFormat('progress-status', {
numResolvedBreaches,
numTotalBreaches
}
),
headlineClassName: "",
};
headlineClassName: ''
}
switch (numResolvedBreaches) {
case 1:
localizedModalStrings.headline = req.fluentFormat("confirmation-1-subhead");
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-1-body");
localizedModalStrings.headlineClassName = "overlay-resolved-first-breach";
break;
localizedModalStrings.headline = req.fluentFormat('confirmation-1-subhead')
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-1-body')
localizedModalStrings.headlineClassName = 'overlay-resolved-first-breach'
break
case 2:
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-2-body");
localizedModalStrings.headlineClassName = "overlay-take-that-hackers";
break;
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-2-body')
localizedModalStrings.headlineClassName = 'overlay-take-that-hackers'
break
case 3:
localizedModalStrings.headline = req.fluentFormat("confirmation-3-subhead");
localizedModalStrings.headline = req.fluentFormat('confirmation-3-subhead')
// TO CONSIDER: The "confirmation-3-body" string contains nested markup.
// We'll either have to remove it (requiring a string change), or we will have
// to inject it into the template using innerHTML (scaryish).
// Defaulting to the generic progressMessage for now.
localizedModalStrings.progressMessage = req.fluentFormat("generic-confirmation-message", {
numUnresolvedBreaches: numTotalBreaches-numResolvedBreaches,
});
localizedModalStrings.headlineClassName = "overlay-another-breach-resolved";
break;
localizedModalStrings.progressMessage = req.fluentFormat('generic-confirmation-message', {
numUnresolvedBreaches: numTotalBreaches - numResolvedBreaches
})
localizedModalStrings.headlineClassName = 'overlay-another-breach-resolved'
break
case numTotalBreaches:
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
localizedModalStrings.progressMessage = req.fluentFormat("progress-complete");
localizedModalStrings.headlineClassName = "overlay-marked-as-resolved";
break;
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
localizedModalStrings.progressMessage = req.fluentFormat('progress-complete')
localizedModalStrings.headlineClassName = 'overlay-marked-as-resolved'
break
default:
if (numResolvedBreaches > 3) {
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-2-body");
localizedModalStrings.headlineClassName = "overlay-marked-as-resolved";
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-2-body')
localizedModalStrings.headlineClassName = 'overlay-marked-as-resolved'
}
break;
break
}
res.json(localizedModalStrings);
res.json(localizedModalStrings)
}
async function postUnsubscribe(req, res) {
const { token, emailHash } = req.body;
async function postUnsubscribe (req, res) {
const { token, emailHash } = req.body
if (!token || !emailHash) {
throw new FluentError("user-unsubscribe-token-email-error");
throw new FluentError('user-unsubscribe-token-email-error')
}
// legacy unsubscribe link page uses removeSubscriberByToken
const unsubscribedUser = await DB.removeSubscriberByToken(token, emailHash);
const unsubscribedUser = await DB.removeSubscriberByToken(token, emailHash)
if (!unsubscribedUser) {
const emailAddress = await DB.getEmailByToken(token);
const emailAddress = await DB.getEmailByToken(token)
if (!emailAddress) {
throw new FluentError("error-not-subscribed");
throw new FluentError('error-not-subscribed')
}
await DB.removeOneSecondaryEmail(emailAddress.id);
return res.redirect("/user/preferences");
await DB.removeOneSecondaryEmail(emailAddress.id)
return res.redirect('/user/preferences')
}
await FXA.revokeOAuthTokens(unsubscribedUser);
req.session.destroy();
res.redirect("/");
await FXA.revokeOAuthTokens(unsubscribedUser)
req.session.destroy()
res.redirect('/')
}
async function getPreferences (req, res) {
const user = req.user
const allBreaches = req.app.locals.breaches
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
async function getPreferences(req, res) {
const user = req.user;
const allBreaches = req.app.locals.breaches;
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
res.render("dashboards", {
title: "Firefox Monitor",
whichPartial: "dashboards/preferences",
res.render('dashboards', {
title: 'Firefox Monitor',
whichPartial: 'dashboards/preferences',
csrfToken: req.csrfToken(),
verifiedEmails, unverifiedEmails,
});
verifiedEmails,
unverifiedEmails
})
}
// This endpoint returns breach stats for Firefox clients to display
// in about:protections
//
@ -536,65 +521,63 @@ async function getPreferences(req, res) {
// with the FXA verification endpoint before we return breach stats.
//
// To test this endpoint, see the "Test Firefox Integration" section of the README.
async function getBreachStats(req, res) {
async function getBreachStats (req, res) {
if (!req.token) {
return res.status(401).json({
errorMessage: "User breach stats requires an FXA OAuth token passed in the Authorization header.",
});
errorMessage: 'User breach stats requires an FXA OAuth token passed in the Authorization header.'
})
}
const fxaResponse = await FXA.verifyOAuthToken(req.token);
if (fxaResponse.name === "HTTPError") {
const fxaResponse = await FXA.verifyOAuthToken(req.token)
if (fxaResponse.name === 'HTTPError') {
return res.status(fxaResponse.response.statusCode).json({
errorMessage: "Could not verify FXA OAuth token. FXA returned message: " + fxaResponse.response.statusMessage,
});
errorMessage: 'Could not verify FXA OAuth token. FXA returned message: ' + fxaResponse.response.statusMessage
})
}
if (!fxaResponse.body.scope.includes(FXA_MONITOR_SCOPE)) {
return res.status(401).json({
errorMessage: "The provided token does not include Monitor scope.",
});
errorMessage: 'The provided token does not include Monitor scope.'
})
}
const user = await DB.getSubscriberByFxaUid(fxaResponse.body.user);
const user = await DB.getSubscriberByFxaUid(fxaResponse.body.user)
if (!user) {
return res.status(404).json({
errorMessage: "Cannot find Monitor subscriber for that FXA.",
});
errorMessage: 'Cannot find Monitor subscriber for that FXA.'
})
}
const allBreaches = req.app.locals.breaches;
const { verifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
const breachStats = resultsSummary(verifiedEmails);
const allBreaches = req.app.locals.breaches
const { verifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
const breachStats = resultsSummary(verifiedEmails)
const baseStats = {
monitoredEmails: breachStats.monitoredEmails.count,
numBreaches: breachStats.numBreaches.count,
passwords: breachStats.passwords.count,
};
passwords: breachStats.passwords.count
}
const resolvedStats = {
numBreachesResolved: breachStats.numBreaches.numResolved,
passwordsResolved: breachStats.passwords.numResolved,
};
const returnStats = (req.query.includeResolved === "true") ? Object.assign(baseStats, resolvedStats) : baseStats;
return res.json(returnStats);
passwordsResolved: breachStats.passwords.numResolved
}
const returnStats = (req.query.includeResolved === 'true') ? Object.assign(baseStats, resolvedStats) : baseStats
return res.json(returnStats)
}
function logout(req, res) {
function logout (req, res) {
// Growth Experiment
if (EXPERIMENTS_ENABLED && req.session.experimentFlags) {
// Persist experimentBranch across session reset
const sessionExperimentFlags = req.session.experimentFlags;
const sessionExperimentFlags = req.session.experimentFlags
req.session.destroy(() => {
req.session = {experimentFlags: sessionExperimentFlags};
});
req.session = { experimentFlags: sessionExperimentFlags }
})
// Return
res.redirect("/");
return;
res.redirect('/')
return
}
req.session.destroy();
res.redirect("/");
req.session.destroy()
res.redirect('/')
}
module.exports = {
FXA_MONITOR_SCOPE,
getPreferences,
@ -612,5 +595,5 @@ module.exports = {
removeEmail,
resendEmail,
updateCommunicationOptions,
resolveBreach,
};
resolveBreach
}

Просмотреть файл

@ -1,26 +1,25 @@
"use strict";
'use strict'
const crypto = require("crypto");
const uuidv4 = require("uuid/v4");
const mozlog = require("../log");
const log = mozlog("controllers.utils");
const crypto = require('crypto')
const uuidv4 = require('uuid/v4')
const mozlog = require('../log')
const log = mozlog('controllers.utils')
const AppConstants = require("../app-constants");
const AppConstants = require('../app-constants')
function generatePageToken (req) {
const pageToken = { ip: req.ip, date: new Date(), nonce: uuidv4() }
function generatePageToken(req) {
const pageToken = { ip: req.ip, date: new Date(), nonce: uuidv4() };
const iv = crypto.randomBytes(16); // AES uses block sizes of 16 bytes
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, "sha512");
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
const iv = crypto.randomBytes(16) // AES uses block sizes of 16 bytes
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, 'sha512')
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
const encryptedPageToken = Buffer.concat([
cipher.update(JSON.stringify(pageToken)),
cipher.final(),
]);
cipher.final()
])
return iv.toString("base64") + "." + encryptedPageToken.toString("base64");
return iv.toString('base64') + '.' + encryptedPageToken.toString('base64')
/* TODO: block on scans-per-ip instead of scans-per-timespan
if (req.session.scans === undefined){
@ -31,204 +30,198 @@ function generatePageToken(req) {
*/
}
function hasUserSignedUpForWaitlist(user, waitlist) {
function hasUserSignedUpForWaitlist (user, waitlist) {
if (!user.waitlists_joined) {
return false;
return false
}
if (user.waitlists_joined.hasOwnProperty(waitlist)) {
return true;
return true
}
return false;
return false
}
function getTotalPercentage(variations) {
let percentage = 0;
// calculate and store total percentage of variations
for (const v in variations) {
if (variations.hasOwnProperty(v) && typeof variations[v] === "number") {
// multiply by 100 to allow for percentages to the hundredth
// (and avoid floating point math errors)
percentage += (variations[v] * 100);
}
function getTotalPercentage (variations) {
let percentage = 0
// calculate and store total percentage of variations
for (const v in variations) {
if (variations.hasOwnProperty(v) && typeof variations[v] === 'number') {
// multiply by 100 to allow for percentages to the hundredth
// (and avoid floating point math errors)
percentage += (variations[v] * 100)
}
}
const totalPercentage = percentage / 100;
const totalPercentage = percentage / 100
// Make sure totalPercent is between 0 and 100
if (totalPercentage === 0 || totalPercentage > 100) {
throw new Error(`The total percentage ${totalPercentage} is out of bounds!`);
}
// Make sure totalPercent is between 0 and 100
if (totalPercentage === 0 || totalPercentage > 100) {
throw new Error(`The total percentage ${totalPercentage} is out of bounds!`)
}
return percentage;
return percentage
}
function chooseVariation(variations, sorterNum){
const totalPercentage = getTotalPercentage(variations);
function chooseVariation (variations, sorterNum) {
const totalPercentage = getTotalPercentage(variations)
// make sure random number falls in the distribution range
let runningTotal;
let choice;
let runningTotal
let choice
if (sorterNum <= totalPercentage) {
runningTotal = 0;
runningTotal = 0
// loop through all variations
for (const v in variations) {
// check if random number falls within current variation range
if (sorterNum <= (variations[v] + runningTotal)) {
// if so, we have found our variation
choice = v;
break;
}
// tally variation percentages for the next loop iteration
runningTotal += variations[v];
// loop through all variations
for (const v in variations) {
// check if random number falls within current variation range
if (sorterNum <= (variations[v] + runningTotal)) {
// if so, we have found our variation
choice = v
break
}
// tally variation percentages for the next loop iteration
runningTotal += variations[v]
}
return choice;
}
function unEnrollSession(session) {
session.excludeFromExperiment = true;
session.experimentBranch = false;
session.treatmentBranch = false;
session.controlBranch = false;
}
function setBranchVariable(branch, sessionExperimentFlags) {
if (branch === "va") {
sessionExperimentFlags.controlBranch = true;
sessionExperimentFlags.treatmentBranch = false;
}
if (branch === "vb") {
sessionExperimentFlags.treatmentBranch = true;
sessionExperimentFlags.controlBranch = false;
return choice
}
function unEnrollSession (session) {
session.excludeFromExperiment = true
session.experimentBranch = false
session.treatmentBranch = false
session.controlBranch = false
}
function setBranchVariable (branch, sessionExperimentFlags) {
if (branch === 'va') {
sessionExperimentFlags.controlBranch = true
sessionExperimentFlags.treatmentBranch = false
}
if (branch === 'vb') {
sessionExperimentFlags.treatmentBranch = true
sessionExperimentFlags.controlBranch = false
}
}
function getExperimentBranch(req, sorterNum = false, language = false, variations) {
const sessionExperimentFlags = req.session.experimentFlags;
function getExperimentBranch (req, sorterNum = false, language = false, variations) {
const sessionExperimentFlags = req.session.experimentFlags
if (sessionExperimentFlags.excludeFromExperiment && !req.query.experimentBranch) {
log.debug("This session has already been excluded from the experiment");
unEnrollSession(sessionExperimentFlags);
return false;
log.debug('This session has already been excluded from the experiment')
unEnrollSession(sessionExperimentFlags)
return false
}
// If we cannot parse req.headers["accept-language"], we should not
// enroll users in the experiment.
if (language && !req.headers || language && !req.headers["accept-language"]){
log.debug("No headers or accept-language information present.");
unEnrollSession(sessionExperimentFlags);
return false;
if (language && !req.headers || language && !req.headers['accept-language']) {
log.debug('No headers or accept-language information present.')
unEnrollSession(sessionExperimentFlags)
return false
}
// If the user doesn't have the requested variant langauge selected as their primary language,
// we do not enroll them in the experiment.
if (language) {
if (!Array.isArray(language)) {
throw new Error("The language param is not an array");
throw new Error('The language param is not an array')
}
const lang = req.headers["accept-language"].split(",");
const lang = req.headers['accept-language'].split(',')
// Check to make sure one of the experiment langauge(s) is the top-preferred language.
const firstLangMatch = (element) => lang[0].includes(element);
const firstLangMatch = (element) => lang[0].includes(element)
if (language && !language.some(firstLangMatch)) {
log.debug(`Preferred language is not [${language}] variant: ${lang[0]}`);
unEnrollSession(sessionExperimentFlags);
return false;
log.debug(`Preferred language is not [${language}] variant: ${lang[0]}`)
unEnrollSession(sessionExperimentFlags)
return false
}
}
// If URL param has experimentBranch entry, use that branch;
if (req.query.experimentBranch) {
if (!Object.keys(variations).includes(req.query.experimentBranch)) {
log.debug("The requested branch is unknown: ", req.query.experimentBranch);
unEnrollSession(sessionExperimentFlags);
return false;
log.debug('The requested branch is unknown: ', req.query.experimentBranch)
unEnrollSession(sessionExperimentFlags)
return false
}
log.debug("This session has been set to the requested branch: ", req.query.experimentBranch);
sessionExperimentFlags.excludeFromExperiment = false;
sessionExperimentFlags.experimentBranch = req.query.experimentBranch;
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
return req.query.experimentBranch;
log.debug('This session has been set to the requested branch: ', req.query.experimentBranch)
sessionExperimentFlags.excludeFromExperiment = false
sessionExperimentFlags.experimentBranch = req.query.experimentBranch
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
return req.query.experimentBranch
}
// If user was already assigned a branch, stay in that branch;
if (sessionExperimentFlags.experimentBranch) {
log.debug("This session has already been assigned: ", sessionExperimentFlags.experimentBranch);
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
return sessionExperimentFlags.experimentBranch;
log.debug('This session has already been assigned: ', sessionExperimentFlags.experimentBranch)
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
return sessionExperimentFlags.experimentBranch
}
if (sorterNum === false) {
sorterNum = Math.floor(Math.random() * 10000) + 1;
sorterNum = sorterNum/100;
sorterNum = Math.floor(Math.random() * 10000) + 1
sorterNum = sorterNum / 100
// sorterNum = Math.floor(Math.random() * 100);
log.debug("No coinflip number provided. Coinflip number is ", sorterNum);
log.debug('No coinflip number provided. Coinflip number is ', sorterNum)
} else {
log.debug("Coinflip number provided. Coinflip number is ", sorterNum);
log.debug('Coinflip number provided. Coinflip number is ', sorterNum)
}
const assignedCohort = chooseVariation(variations, sorterNum);
const assignedCohort = chooseVariation(variations, sorterNum)
if (!assignedCohort) {
log.debug("This session has randomly been removed from the experiment");
sessionExperimentFlags.excludeFromExperiment = true;
return false;
log.debug('This session has randomly been removed from the experiment')
sessionExperimentFlags.excludeFromExperiment = true
return false
}
log.debug(`This session has been randomly assigned to the ${assignedCohort} cohort.`);
sessionExperimentFlags.experimentBranch = assignedCohort;
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
return assignedCohort;
log.debug(`This session has been randomly assigned to the ${assignedCohort} cohort.`)
sessionExperimentFlags.experimentBranch = assignedCohort
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
return assignedCohort
}
function getUTMContents(req) {
function getUTMContents (req) {
if (!req) {
throw new Error("No request available");
throw new Error('No request available')
}
// If UTMs are set previously, set them again.
if (req.session.utmOverrides) {
return req.session.utmOverrides;
return req.session.utmOverrides
}
req.session.utmOverrides = false;
return false;
req.session.utmOverrides = false
return false
}
function getExperimentFlags(req, EXPERIMENTS_ENABLED) {
function getExperimentFlags (req, EXPERIMENTS_ENABLED) {
if (!req) {
throw new Error("No request available");
throw new Error('No request available')
}
if (req.session.experimentFlags && EXPERIMENTS_ENABLED) {
return req.session.experimentFlags;
return req.session.experimentFlags
}
const experimentFlags = {
experimentBranch: false,
treatmentBranch: false,
controlBranch: false,
excludeFromExperiment: false,
};
excludeFromExperiment: false
}
req.session.experimentFlags = experimentFlags;
return experimentFlags;
req.session.experimentFlags = experimentFlags
return experimentFlags
}
module.exports = {
generatePageToken,
hasUserSignedUpForWaitlist,
getExperimentBranch,
getExperimentFlags,
getUTMContents,
};
getUTMContents
}

509
db/DB.js
Просмотреть файл

@ -1,193 +1,189 @@
"use strict";
'use strict'
// eslint-disable-next-line node/no-extraneous-require
const uuidv4 = require("uuid/v4");
const Knex = require("knex");
const { attachPaginate } = require("knex-paginate");
const uuidv4 = require('uuid/v4')
const Knex = require('knex')
const { attachPaginate } = require('knex-paginate')
const { FluentError } = require("../locale-utils");
const AppConstants = require("../app-constants");
const { FXA } = require("../lib/fxa");
const HIBP = require("../hibp");
const getSha1 = require("../sha1-utils");
const mozlog = require("../log");
const { FluentError } = require('../locale-utils')
const AppConstants = require('../app-constants')
const { FXA } = require('../lib/fxa')
const HIBP = require('../hibp')
const getSha1 = require('../sha1-utils')
const mozlog = require('../log')
const knexConfig = require("./knexfile");
const knexConfig = require('./knexfile')
let knex = Knex(knexConfig);
attachPaginate();
const log = mozlog("DB");
let knex = Knex(knexConfig)
attachPaginate()
const log = mozlog('DB')
const DB = {
async getSubscriberByToken(token) {
const res = await knex("subscribers")
.where("primary_verification_token", "=", token);
async getSubscriberByToken (token) {
const res = await knex('subscribers')
.where('primary_verification_token', '=', token)
return res[0];
return res[0]
},
async getEmailByToken(token) {
const res = await knex("email_addresses")
.where("verification_token", "=", token);
async getEmailByToken (token) {
const res = await knex('email_addresses')
.where('verification_token', '=', token)
return res[0];
return res[0]
},
async getEmailById(emailAddressId) {
const res = await knex("email_addresses")
.where("id", "=", emailAddressId);
async getEmailById (emailAddressId) {
const res = await knex('email_addresses')
.where('id', '=', emailAddressId)
return res[0];
return res[0]
},
async getSubscriberByTokenAndHash(token, emailSha1) {
const res = await knex.table("subscribers")
async getSubscriberByTokenAndHash (token, emailSha1) {
const res = await knex.table('subscribers')
.first()
.where({
"primary_verification_token": token,
"primary_sha1": emailSha1,
});
return res;
primary_verification_token: token,
primary_sha1: emailSha1
})
return res
},
async joinEmailAddressesToSubscriber(subscriber) {
async joinEmailAddressesToSubscriber (subscriber) {
if (subscriber) {
const emailAddressRecords = await knex("email_addresses").where({
"subscriber_id": subscriber.id,
});
const emailAddressRecords = await knex('email_addresses').where({
subscriber_id: subscriber.id
})
subscriber.email_addresses = emailAddressRecords.map(
emailAddress=>({id: emailAddress.id, email: emailAddress.email})
);
emailAddress => ({ id: emailAddress.id, email: emailAddress.email })
)
}
return subscriber;
return subscriber
},
async getSubscriberById(id) {
const [subscriber] = await knex("subscribers").where({
"id": id,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
async getSubscriberById (id) {
const [subscriber] = await knex('subscribers').where({
id
})
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
return subscriberAndEmails
},
async getSubscriberByFxaUid(uid) {
const [subscriber] = await knex("subscribers").where({
"fxa_uid": uid,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
async getSubscriberByFxaUid (uid) {
const [subscriber] = await knex('subscribers').where({
fxa_uid: uid
})
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
return subscriberAndEmails
},
async getPreFxaSubscribersPage(pagination) {
return await knex("subscribers")
async getPreFxaSubscribersPage (pagination) {
return await knex('subscribers')
.whereRaw("(fxa_uid = '') IS NOT FALSE")
.paginate(pagination);
.paginate(pagination)
},
async getSubscriberByEmail(email) {
const [subscriber] = await knex("subscribers").where({
"primary_email": email,
"primary_verified": true,
});
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
async getSubscriberByEmail (email) {
const [subscriber] = await knex('subscribers').where({
primary_email: email,
primary_verified: true
})
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
return subscriberAndEmails
},
async getEmailAddressRecordByEmail(email) {
const emailAddresses = await knex("email_addresses").where({
"email": email, verified: true,
});
async getEmailAddressRecordByEmail (email) {
const emailAddresses = await knex('email_addresses').where({
email, verified: true
})
if (!emailAddresses) {
return null;
return null
}
if (emailAddresses.length > 1) {
// TODO: handle multiple emails in separate(?) subscriber accounts?
log.warn("getEmailAddressRecordByEmail", {msg: "found the same email multiple times"});
log.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
}
return emailAddresses[0];
return emailAddresses[0]
},
async addSubscriberUnverifiedEmailHash(user, email) {
const res = await knex("email_addresses").insert({
async addSubscriberUnverifiedEmailHash (user, email) {
const res = await knex('email_addresses').insert({
subscriber_id: user.id,
email: email,
email,
sha1: getSha1(email),
verification_token: uuidv4(),
verified: false,
}).returning("*");
return res[0];
verified: false
}).returning('*')
return res[0]
},
async resetUnverifiedEmailAddress(emailAddressId) {
const newVerificationToken = uuidv4();
const res = await knex("email_addresses")
async resetUnverifiedEmailAddress (emailAddressId) {
const newVerificationToken = uuidv4()
const res = await knex('email_addresses')
.update({
verification_token: newVerificationToken,
updated_at: knex.fn.now(),
updated_at: knex.fn.now()
})
.where("id", emailAddressId)
.returning("*");
return res[0];
.where('id', emailAddressId)
.returning('*')
return res[0]
},
async verifyEmailHash(token) {
const unverifiedEmail = await this.getEmailByToken(token);
async verifyEmailHash (token) {
const unverifiedEmail = await this.getEmailByToken(token)
if (!unverifiedEmail) {
throw new FluentError("Error message for this verification email timed out or something went wrong.");
throw new FluentError('Error message for this verification email timed out or something went wrong.')
}
const verifiedEmail = await this._verifyNewEmail(unverifiedEmail);
return verifiedEmail[0];
const verifiedEmail = await this._verifyNewEmail(unverifiedEmail)
return verifiedEmail[0]
},
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
// Used internally, ideally should not be called by consumers.
async _getSha1EntryAndDo(sha1, aFoundCallback, aNotFoundCallback) {
const existingEntries = await knex("subscribers")
.where("primary_sha1", sha1);
async _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
const existingEntries = await knex('subscribers')
.where('primary_sha1', sha1)
if (existingEntries.length && aFoundCallback) {
return await aFoundCallback(existingEntries[0]);
return await aFoundCallback(existingEntries[0])
}
if (!existingEntries.length && aNotFoundCallback) {
return await aNotFoundCallback();
return await aNotFoundCallback()
}
},
// Used internally.
async _addEmailHash(sha1, email, signup_language, verified = false) {
async _addEmailHash (sha1, email, signup_language, verified = false) {
try {
return await this._getSha1EntryAndDo(sha1, async aEntry => {
// Entry existed, patch the email value if supplied.
if (email) {
const res = await knex("subscribers")
const res = await knex('subscribers')
.update({
primary_email: email,
primary_sha1: getSha1(email.toLowerCase()),
primary_verified: verified,
updated_at: knex.fn.now(),
updated_at: knex.fn.now()
})
.where("id", "=", aEntry.id)
.returning("*");
return res[0];
.where('id', '=', aEntry.id)
.returning('*')
return res[0]
}
return aEntry;
return aEntry
}, async () => {
// Always add a verification_token value
const verification_token = uuidv4();
const res = await knex("subscribers")
const verification_token = uuidv4()
const res = await knex('subscribers')
.insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language, primary_verification_token: verification_token, primary_verified: verified })
.returning("*");
return res[0];
});
.returning('*')
return res[0]
})
} catch (e) {
throw new FluentError("error-could-not-add-email");
throw new FluentError('error-could-not-add-email')
}
},
@ -204,14 +200,14 @@ const DB = {
* @param {string} fxaProfileData from Firefox Account
* @returns {object} subscriber knex object added to DB
*/
async addSubscriber(email, signupLanguage, fxaAccessToken=null, fxaRefreshToken=null, fxaProfileData=null) {
const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true);
const verified = await this._verifySubscriber(emailHash);
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null;
async addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true)
const verified = await this._verifySubscriber(emailHash)
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
if (fxaRefreshToken || fxaProfileData) {
return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData);
return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
}
return verifiedSubscriber;
return verifiedSubscriber
},
/**
@ -223,42 +219,41 @@ const DB = {
* @param {object} emailHash knex object in DB
* @returns {object} verified subscriber knex object in DB
*/
async _verifySubscriber(emailHash) {
async _verifySubscriber (emailHash) {
// TODO: move this "up" into controllers/users ?
await HIBP.subscribeHash(emailHash.primary_sha1);
await HIBP.subscribeHash(emailHash.primary_sha1)
const verifiedSubscriber = await knex("subscribers")
.where("primary_email", "=", emailHash.primary_email)
const verifiedSubscriber = await knex('subscribers')
.where('primary_email', '=', emailHash.primary_email)
.update({
primary_verified: true,
updated_at: knex.fn.now(),
updated_at: knex.fn.now()
})
.returning("*");
.returning('*')
return verifiedSubscriber;
return verifiedSubscriber
},
// Verifies new emails added by existing users
async _verifyNewEmail(emailHash) {
await HIBP.subscribeHash(emailHash.sha1);
async _verifyNewEmail (emailHash) {
await HIBP.subscribeHash(emailHash.sha1)
const verifiedEmail = await knex("email_addresses")
.where("id", "=", emailHash.id)
const verifiedEmail = await knex('email_addresses')
.where('id', '=', emailHash.id)
.update({
verified: true,
verified: true
})
.returning("*");
.returning('*')
return verifiedEmail;
return verifiedEmail
},
async getUserEmails(userId) {
async getUserEmails (userId) {
const userEmails = await knex('email_addresses')
.where('subscriber_id', '=', userId)
.returning('*')
const userEmails = await knex("email_addresses")
.where("subscriber_id", "=", userId)
.returning("*");
return userEmails;
return userEmails
},
/**
@ -270,174 +265,172 @@ const DB = {
* @param {string} fxaProfileData from Firefox Account
* @returns {object} updated subscriber knex object in DB
*/
async _updateFxAData(subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
const fxaUID = JSON.parse(fxaProfileData).uid;
const updated = await knex("subscribers")
.where("id", "=", subscriber.id)
.update({
fxa_uid: fxaUID,
fxa_access_token: fxaAccessToken,
fxa_refresh_token: fxaRefreshToken,
fxa_profile_json: fxaProfileData,
})
.returning("*");
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
if (updatedSubscriber) {
FXA.destroyOAuthToken({refresh_token: subscriber.fxa_refresh_token});
}
return updatedSubscriber;
},
async updateFxAProfileData(subscriber, fxaProfileData) {
await knex("subscribers").where("id", subscriber.id)
.update({
fxa_profile_json: fxaProfileData,
});
return this.getSubscriberById(subscriber.id);
},
async setBreachesLastShownNow(subscriber) {
// TODO: turn 2 db queries into a single query (also see #942)
const nowDateTime = new Date();
const nowTimeStamp = nowDateTime.toISOString();
await knex("subscribers")
.where("id", "=", subscriber.id)
.update({
breaches_last_shown: nowTimeStamp,
});
return this.getSubscriberByEmail(subscriber.primary_email);
},
async setAllEmailsToPrimary(subscriber, allEmailsToPrimary) {
const updated = await knex("subscribers")
.where("id", subscriber.id)
.update({
all_emails_to_primary: allEmailsToPrimary,
})
.returning("*");
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
return updatedSubscriber;
},
async setBreachesResolved(options) {
const { user, updatedResolvedBreaches } = options;
await knex("subscribers")
.where("id", user.id)
.update({
breaches_resolved: updatedResolvedBreaches,
});
return this.getSubscriberByEmail(user.primary_email);
},
async setWaitlistsJoined(options) {
const { user, updatedWaitlistsJoined } = options;
await knex("subscribers")
.where("id", user.id)
async _updateFxAData (subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
const fxaUID = JSON.parse(fxaProfileData).uid
const updated = await knex('subscribers')
.where('id', '=', subscriber.id)
.update({
waitlists_joined: updatedWaitlistsJoined,
});
return this.getSubscriberByEmail(user.primary_email);
fxa_uid: fxaUID,
fxa_access_token: fxaAccessToken,
fxa_refresh_token: fxaRefreshToken,
fxa_profile_json: fxaProfileData
})
.returning('*')
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
if (updatedSubscriber) {
FXA.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token })
}
return updatedSubscriber
},
async removeSubscriber(subscriber) {
await knex("email_addresses").where({"subscriber_id": subscriber.id}).del();
await knex("subscribers").where({"id": subscriber.id}).del();
async updateFxAProfileData (subscriber, fxaProfileData) {
await knex('subscribers').where('id', subscriber.id)
.update({
fxa_profile_json: fxaProfileData
})
return this.getSubscriberById(subscriber.id)
},
async setBreachesLastShownNow (subscriber) {
// TODO: turn 2 db queries into a single query (also see #942)
const nowDateTime = new Date()
const nowTimeStamp = nowDateTime.toISOString()
await knex('subscribers')
.where('id', '=', subscriber.id)
.update({
breaches_last_shown: nowTimeStamp
})
return this.getSubscriberByEmail(subscriber.primary_email)
},
async setAllEmailsToPrimary (subscriber, allEmailsToPrimary) {
const updated = await knex('subscribers')
.where('id', subscriber.id)
.update({
all_emails_to_primary: allEmailsToPrimary
})
.returning('*')
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
return updatedSubscriber
},
async setBreachesResolved (options) {
const { user, updatedResolvedBreaches } = options
await knex('subscribers')
.where('id', user.id)
.update({
breaches_resolved: updatedResolvedBreaches
})
return this.getSubscriberByEmail(user.primary_email)
},
async setWaitlistsJoined (options) {
const { user, updatedWaitlistsJoined } = options
await knex('subscribers')
.where('id', user.id)
.update({
waitlists_joined: updatedWaitlistsJoined
})
return this.getSubscriberByEmail(user.primary_email)
},
async removeSubscriber (subscriber) {
await knex('email_addresses').where({ subscriber_id: subscriber.id }).del()
await knex('subscribers').where({ id: subscriber.id }).del()
},
// This is used by SES callbacks to remove email addresses when recipients
// perma-bounce or mark our emails as spam
// Removes from either subscribers or email_addresses as necessary
async removeEmail(email) {
const subscriber = await this.getSubscriberByEmail(email);
async removeEmail (email) {
const subscriber = await this.getSubscriberByEmail(email)
if (!subscriber) {
const emailAddress = await this.getEmailAddressRecordByEmail(email);
const emailAddress = await this.getEmailAddressRecordByEmail(email)
if (!emailAddress) {
log.warn("removed-subscriber-not-found");
return;
log.warn('removed-subscriber-not-found')
return
}
await knex("email_addresses")
await knex('email_addresses')
.where({
"email": email,
"verified": true,
email,
verified: true
})
.del();
return;
.del()
return
}
// This can fail if a subscriber has more email_addresses and marks
// a primary email as spam, but we should let it fail so we can see it
// in the logs
await knex("subscribers")
await knex('subscribers')
.where({
"primary_verification_token": subscriber.primary_verification_token,
"primary_sha1": subscriber.primary_sha1,
primary_verification_token: subscriber.primary_verification_token,
primary_sha1: subscriber.primary_sha1
})
.del();
return;
.del()
},
async removeSubscriberByToken(token, emailSha1) {
const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1);
async removeSubscriberByToken (token, emailSha1) {
const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1)
if (!subscriber) {
return false;
return false
}
await knex("subscribers")
await knex('subscribers')
.where({
"primary_verification_token": subscriber.primary_verification_token,
"primary_sha1": subscriber.primary_sha1,
primary_verification_token: subscriber.primary_verification_token,
primary_sha1: subscriber.primary_sha1
})
.del();
return subscriber;
.del()
return subscriber
},
async removeOneSecondaryEmail(emailId) {
await knex("email_addresses")
async removeOneSecondaryEmail (emailId) {
await knex('email_addresses')
.where({
"id": emailId,
id: emailId
})
.del();
return;
.del()
},
async getSubscribersByHashes(hashes) {
return await knex("subscribers").whereIn("primary_sha1", hashes).andWhere("primary_verified", "=", true);
async getSubscribersByHashes (hashes) {
return await knex('subscribers').whereIn('primary_sha1', hashes).andWhere('primary_verified', '=', true)
},
async getEmailAddressesByHashes(hashes) {
return await knex("email_addresses")
.join("subscribers", "email_addresses.subscriber_id", "=", "subscribers.id")
.whereIn("email_addresses.sha1", hashes)
.andWhere("email_addresses.verified", "=", true);
async getEmailAddressesByHashes (hashes) {
return await knex('email_addresses')
.join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
.whereIn('email_addresses.sha1', hashes)
.andWhere('email_addresses.verified', '=', true)
},
async deleteUnverifiedSubscribers() {
const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000);
const expiredTimeStamp = expiredDateTime.toISOString();
const numDeleted = await knex("subscribers")
.where("primary_verified", false)
.andWhere("created_at", "<", expiredTimeStamp)
.del();
log.info("deleteUnverifiedSubscribers", {msg:`Deleted ${numDeleted} rows.`});
async deleteUnverifiedSubscribers () {
const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000)
const expiredTimeStamp = expiredDateTime.toISOString()
const numDeleted = await knex('subscribers')
.where('primary_verified', false)
.andWhere('created_at', '<', expiredTimeStamp)
.del()
log.info('deleteUnverifiedSubscribers', { msg: `Deleted ${numDeleted} rows.` })
},
async deleteSubscriberByFxAUID(fxaUID) {
await knex("subscribers").where("fxa_uid", fxaUID).del();
async deleteSubscriberByFxAUID (fxaUID) {
await knex('subscribers').where('fxa_uid', fxaUID).del()
},
async deleteEmailAddressesByUid(uid) {
await knex("email_addresses").where({"subscriber_id": uid}).del();
async deleteEmailAddressesByUid (uid) {
await knex('email_addresses').where({ subscriber_id: uid }).del()
},
async createConnection() {
async createConnection () {
if (knex === null) {
knex = Knex(knexConfig);
knex = Knex(knexConfig)
}
},
async destroyConnection() {
await knex.destroy();
knex = null;
},
async destroyConnection () {
await knex.destroy()
knex = null
}
};
}
module.exports = DB;
module.exports = DB

Просмотреть файл

@ -1,31 +1,29 @@
"use strict";
'use strict'
// eslint-disable-next-line node/no-extraneous-require
const { parse } = require("pg-connection-string");
const { parse } = require('pg-connection-string')
const AppConstants = require("../app-constants");
const connectionObj = parse(AppConstants.DATABASE_URL);
if (AppConstants.NODE_ENV === "heroku") {
connectionObj.ssl = {rejectUnauthorized: false};
const AppConstants = require('../app-constants')
const connectionObj = parse(AppConstants.DATABASE_URL)
if (AppConstants.NODE_ENV === 'heroku') {
connectionObj.ssl = { rejectUnauthorized: false }
}
// For runtime, use DATABASE_URL
const RUNTIME_CONFIG = {
client: "postgresql",
connection: connectionObj,
};
client: 'postgresql',
connection: connectionObj
}
// For tests, use test-DATABASE
const testConnectionObj = parse(AppConstants.DATABASE_URL.replace(/\/(\w*)$/, "/test-$1"));
const testConnectionObj = parse(AppConstants.DATABASE_URL.replace(/\/(\w*)$/, '/test-$1'))
const TESTS_CONFIG = {
client: "postgresql",
connection: testConnectionObj,
};
if (AppConstants.NODE_ENV === "tests" ) {
module.exports = TESTS_CONFIG;
} else {
module.exports = RUNTIME_CONFIG;
client: 'postgresql',
connection: testConnectionObj
}
if (AppConstants.NODE_ENV === 'tests') {
module.exports = TESTS_CONFIG
} else {
module.exports = RUNTIME_CONFIG
}

Просмотреть файл

@ -1,17 +1,17 @@
"use strict";
'use strict'
exports.up = knex => {
return knex.schema
.createTable("subscribers", table => {
table.increments("id").primary();
table.string("sha1");
table.string("email");
table.string("verification_token").unique();
table.boolean("verified").defaultTo(false);
});
};
.createTable('subscribers', table => {
table.increments('id').primary()
table.string('sha1')
table.string('email')
table.string('verification_token').unique()
table.boolean('verified').defaultTo(false)
})
}
exports.down = knex => {
return knex.schema
.dropTableIfExists("subscribers");
};
.dropTableIfExists('subscribers')
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.timestamps(false, true);
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.timestamps(false, true)
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropTimestamps();
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropTimestamps()
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.boolean("fx_newsletter").defaultTo(false);
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.boolean('fx_newsletter').defaultTo(false)
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("fx_newsletter");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('fx_newsletter')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.string("signup_language");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.string('signup_language')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("signup_language");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('signup_language')
})
}

Просмотреть файл

@ -1,17 +1,16 @@
"use strict";
'use strict'
// Note: this index was created on heroku, stage, and prod by hand
// Use this statement to "fake" the migration:
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20181007085241_add_sha1_index.js', 4, '2018-10-07 08:52:42.000-05');
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.index("sha1", "subscribers_sha1_idx");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.index('sha1', 'subscribers_sha1_idx')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropIndex("sha1", "subscribers_sha1_idx");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropIndex('sha1', 'subscribers_sha1_idx')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.index("created_at");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.index('created_at')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropIndex("created_at");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropIndex('created_at')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.index("email", "subscribers_email_idx");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.index('email', 'subscribers_email_idx')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropIndex("email", "subscribers_email_idx");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropIndex('email', 'subscribers_email_idx')
})
}

Просмотреть файл

@ -1,15 +1,15 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.string("fxa_refresh_token");
table.jsonb("fxa_profile_json");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.string('fxa_refresh_token')
table.jsonb('fxa_profile_json')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("fxa_refresh_token");
table.dropColumn("fxa_profile_json");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('fxa_refresh_token')
table.dropColumn('fxa_profile_json')
})
}

Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.index('verified', 'subscribers_verified_idx')
})
}
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.index("verified", "subscribers_verified_idx");
});
};
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropIndex("verified", "subscribers_verified_idx");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropIndex('verified', 'subscribers_verified_idx')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.string("fxa_uid");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.string('fxa_uid')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("fxa_uid");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('fxa_uid')
})
}

Просмотреть файл

@ -1,34 +1,34 @@
"use strict";
'use strict'
exports.up = function(knex) {
exports.up = function (knex) {
return Promise.all([
knex.schema.createTable("email_addresses", table => {
table.increments("id").primary();
table.integer("subscriber_id").references("subscribers.id").notNullable();
table.string("sha1");
table.string("email");
table.string("verification_token").unique();
table.boolean("verified").defaultTo(false);
knex.schema.createTable('email_addresses', table => {
table.increments('id').primary()
table.integer('subscriber_id').references('subscribers.id').notNullable()
table.string('sha1')
table.string('email')
table.string('verification_token').unique()
table.boolean('verified').defaultTo(false)
}),
knex.schema.alterTable("subscribers", table => {
table.renameColumn("sha1", "primary_sha1");
table.renameColumn("email", "primary_email");
table.renameColumn("verification_token", "primary_verification_token");
table.renameColumn("verified", "primary_verified");
}),
]);
};
knex.schema.alterTable('subscribers', table => {
table.renameColumn('sha1', 'primary_sha1')
table.renameColumn('email', 'primary_email')
table.renameColumn('verification_token', 'primary_verification_token')
table.renameColumn('verified', 'primary_verified')
})
])
}
exports.down = function(knex) {
exports.down = function (knex) {
return Promise.all([
knex.schema.dropTableIfExists("email_addresses"),
knex.schema.dropTableIfExists('email_addresses'),
knex.schema.alterTable("subscribers", table => {
table.renameColumn("primary_sha1", "sha1");
table.renameColumn("primary_email", "email");
table.renameColumn("primary_verification_token", "verification_token");
table.renameColumn("primary_verified", "verified");
}),
]);
};
knex.schema.alterTable('subscribers', table => {
table.renameColumn('primary_sha1', 'sha1')
table.renameColumn('primary_email', 'email')
table.renameColumn('primary_verification_token', 'verification_token')
table.renameColumn('primary_verified', 'verified')
})
])
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.timestamp("breaches_last_shown");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.timestamp('breaches_last_shown')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("breaches_last_shown");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('breaches_last_shown')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("email_addresses", table => {
table.timestamps(false);
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('email_addresses', table => {
table.timestamps(false)
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("email_addresses", table => {
table.dropTimestamps();
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('email_addresses', table => {
table.dropTimestamps()
})
}

Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.boolean('all_emails_to_primary').defaultTo(false)
})
}
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.boolean("all_emails_to_primary").defaultTo(false);
});
};
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("all_emails_to_primary");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('all_emails_to_primary')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.string("fxa_access_token");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.string('fxa_access_token')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("subscribers", table => {
table.dropColumn("fxa_access_token");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('subscribers', table => {
table.dropColumn('fxa_access_token')
})
}

Просмотреть файл

@ -1,17 +1,16 @@
"use strict";
'use strict'
// Note: this index was created on heroku, stage, and prod by hand
// Use this statement to "fake" the migration:
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20190713193852_add_email_sha1_index.js', (SELECT max(batch) + 1 FROM knex_migrations), '2019-07-13 19:52:42.000-05');
exports.up = function(knex, Promise) {
return knex.schema.table("email_addresses", table => {
table.index("sha1", "email_addresses_sha1_idx");
});
};
exports.up = function (knex, Promise) {
return knex.schema.table('email_addresses', table => {
table.index('sha1', 'email_addresses_sha1_idx')
})
}
exports.down = function(knex, Promise) {
return knex.schema.table("email_addresses", table => {
table.dropIndex("sha1", "email_addresses_sha1_idx");
});
};
exports.down = function (knex, Promise) {
return knex.schema.table('email_addresses', table => {
table.dropIndex('sha1', 'email_addresses_sha1_idx')
})
}

Просмотреть файл

@ -1,18 +1,17 @@
"use strict";
'use strict'
// Note: this index was created on stage and prod by hand
// Use this statement to "fake" the migration:
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20191118100718_add-fxa-uid-index.js', (SELECT max(batch) + 1 FROM knex_migrations), '2019-11-18 11:00:00.000-05');
exports.up = function(knex) {
return knex.schema.table("subscribers", table => {
table.index("fxa_uid", "subscribers_fxa_uid_idx");
});
};
exports.up = function (knex) {
return knex.schema.table('subscribers', table => {
table.index('fxa_uid', 'subscribers_fxa_uid_idx')
})
}
exports.down = function(knex) {
return knex.schema.table("subscribers", table => {
table.dropIndex("fxa_uid", "subscribers_fxa_uid_idx");
});
};
exports.down = function (knex) {
return knex.schema.table('subscribers', table => {
table.dropIndex('fxa_uid', 'subscribers_fxa_uid_idx')
})
}

Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
exports.up = function (knex) {
return knex.schema.table('email_addresses', table => {
table.index('email', 'email_addresses_email_idx')
})
}
exports.up = function(knex) {
return knex.schema.table("email_addresses", table => {
table.index("email", "email_addresses_email_idx");
});
};
exports.down = function(knex) {
return knex.schema.table("email_addresses", table => {
table.dropIndex("email", "email_addresses_email_idx");
});
};
exports.down = function (knex) {
return knex.schema.table('email_addresses', table => {
table.dropIndex('email', 'email_addresses_email_idx')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex) {
return knex.schema.table("subscribers", table => {
table.jsonb("breaches_resolved");
});
};
exports.up = function (knex) {
return knex.schema.table('subscribers', table => {
table.jsonb('breaches_resolved')
})
}
exports.down = function(knex) {
return knex.schema.table("subscribers", table => {
table.dropColumn("breaches_resolved");
});
};
exports.down = function (knex) {
return knex.schema.table('subscribers', table => {
table.dropColumn('breaches_resolved')
})
}

Просмотреть файл

@ -1,13 +1,13 @@
"use strict";
'use strict'
exports.up = function(knex) {
return knex.schema.table("subscribers", table => {
table.jsonb("waitlists_joined");
});
};
exports.up = function (knex) {
return knex.schema.table('subscribers', table => {
table.jsonb('waitlists_joined')
})
}
exports.down = function(knex) {
return knex.schema.table("subscribers", table => {
table.dropColumn("waitlists_joined");
});
};
exports.down = function (knex) {
return knex.schema.table('subscribers', table => {
table.dropColumn('waitlists_joined')
})
}

Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
exports.up = function (knex) {
return knex.schema.table('subscribers', table => {
table.index('breaches_last_shown', 'subscribers_breaches_last_shown_idx')
})
}
exports.up = function(knex) {
return knex.schema.table("subscribers", table => {
table.index("breaches_last_shown", "subscribers_breaches_last_shown_idx");
});
};
exports.down = function(knex) {
return knex.schema.table("email_addresses", table => {
table.dropIndex("breaches_last_shown", "subscribers_breaches_last_shown_idx");
});
};
exports.down = function (knex) {
return knex.schema.table('email_addresses', table => {
table.dropIndex('breaches_last_shown', 'subscribers_breaches_last_shown_idx')
})
}

Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
exports.up = function (knex) {
return knex.schema.table('subscribers', table => {
table.index('signup_language', 'subscribers_signup_language_idx')
})
}
exports.up = function(knex) {
return knex.schema.table("subscribers", table => {
table.index("signup_language", "subscribers_signup_language_idx");
});
};
exports.down = function(knex) {
return knex.schema.table("email_addresses", table => {
table.dropIndex("signup_language", "subscribers_signup_language_idx");
});
};
exports.down = function (knex) {
return knex.schema.table('email_addresses', table => {
table.dropIndex('signup_language', 'subscribers_signup_language_idx')
})
}

Просмотреть файл

@ -1,70 +1,69 @@
"use strict";
const getSha1 = require("../../sha1-utils");
'use strict'
const getSha1 = require('../../sha1-utils')
exports.TEST_SUBSCRIBERS = {
firefox_account: {
id: 12345,
primary_sha1: getSha1("firefoxaccount@test.com"),
primary_email: "firefoxaccount@test.com",
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf1",
primary_sha1: getSha1('firefoxaccount@test.com'),
primary_email: 'firefoxaccount@test.com',
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf1',
primary_verified: true,
fxa_access_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc0",
fxa_refresh_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc1",
fxa_access_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc0',
fxa_refresh_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc1',
fxa_uid: 12345,
fxa_profile_json: {},
breaches_last_shown: "2019-04-24 13:27:08.421-05",
breaches_resolved: {"firefoxaccount@test.com": [0]},
breaches_last_shown: '2019-04-24 13:27:08.421-05',
breaches_resolved: { 'firefoxaccount@test.com': [0] }
},
all_emails_to_primary: {
id: 67890,
primary_sha1: getSha1("all_emails_to_primary@test.com"),
primary_email: "all_emails_to_primary@test.com",
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2ca7",
primary_sha1: getSha1('all_emails_to_primary@test.com'),
primary_email: 'all_emails_to_primary@test.com',
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2ca7',
primary_verified: true,
fxa_refresh_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc2",
breaches_last_shown: "2019-04-24 13:27:08.421-05",
all_emails_to_primary: true,
fxa_refresh_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc2',
breaches_last_shown: '2019-04-24 13:27:08.421-05',
all_emails_to_primary: true
},
unverified_email: {
primary_sha1: getSha1("unverifiedemail@test.com"),
primary_email: "unverifiedemail@test.com",
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf0",
primary_verified: false,
primary_sha1: getSha1('unverifiedemail@test.com'),
primary_email: 'unverifiedemail@test.com',
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf0',
primary_verified: false
},
verified_email: {
primary_sha1: getSha1("verifiedemail@test.com"),
primary_email: "verifiedemail@test.com",
primary_verification_token: "54010800-6c3c-4186-971a-76dc92874941",
primary_sha1: getSha1('verifiedemail@test.com'),
primary_email: 'verifiedemail@test.com',
primary_verification_token: '54010800-6c3c-4186-971a-76dc92874941',
primary_verified: true,
signup_language: "en-US;q=0.7,en;q=0.3",
},
};
signup_language: 'en-US;q=0.7,en;q=0.3'
}
}
exports.TEST_EMAIL_ADDRESSES = {
firefox_account: {
id: 11111,
subscriber_id: 12345,
sha1: getSha1("firefoxaccount-secondary@test.com"),
email: "firefoxaccount-secondary@test.com",
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf2",
verified: true,
sha1: getSha1('firefoxaccount-secondary@test.com'),
email: 'firefoxaccount-secondary@test.com',
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf2',
verified: true
},
unverified_email_on_firefox_account: {
id: 98765,
subscriber_id: 12345,
sha1: getSha1("firefoxaccount-tertiary@test.com"),
email: "firefoxaccount-tertiary@test.com",
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf3",
verified: false,
sha1: getSha1('firefoxaccount-tertiary@test.com'),
email: 'firefoxaccount-tertiary@test.com',
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf3',
verified: false
},
all_emails_to_primary: {
id: 99999,
subscriber_id: 67890,
sha1: getSha1("secondary_sending_to_primary@test.com"),
email: "secondary_sending_to_primary@test.com",
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf4",
verified: true,
},
};
sha1: getSha1('secondary_sending_to_primary@test.com'),
email: 'secondary_sending_to_primary@test.com',
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf4',
verified: true
}
}

Просмотреть файл

@ -1,60 +1,57 @@
"use strict";
const { URL } = require("url");
'use strict'
const { URL } = require('url')
const AppConstants = require("./app-constants");
const AppConstants = require('./app-constants')
const nodemailer = require("nodemailer");
const hbs = require("nodemailer-express-handlebars");
const nodemailer = require('nodemailer')
const hbs = require('nodemailer-express-handlebars')
const HBSHelpers = require("./template-helpers/");
const mozlog = require("./log");
const HBSHelpers = require('./template-helpers/')
const mozlog = require('./log')
const log = mozlog("email-utils");
const log = mozlog('email-utils')
const hbsOptions = {
viewEngine: {
extname: ".hbs",
layoutsDir: __dirname + "/views/layouts",
defaultLayout: "default_email",
partialsDir: __dirname + "/views/partials",
helpers: HBSHelpers.helpers,
extname: '.hbs',
layoutsDir: __dirname + '/views/layouts',
defaultLayout: 'default_email',
partialsDir: __dirname + '/views/partials',
helpers: HBSHelpers.helpers
},
viewPath: __dirname + "/views/layouts",
extName: ".hbs",
};
viewPath: __dirname + '/views/layouts',
extName: '.hbs'
}
// The SMTP transport object. This is initialized to a nodemailer transport
// object while reading SMTP credentials, or to a dummy function in debug mode.
let gTransporter;
let gTransporter
const EmailUtils = {
async init(smtpUrl = AppConstants.SMTP_URL) {
async init (smtpUrl = AppConstants.SMTP_URL) {
// Allow a debug mode that will log JSON instead of sending emails.
if (!smtpUrl) {
log.info("smtpUrl-empty", { message: "EmailUtils will log a JSON response instead of sending emails." });
gTransporter = nodemailer.createTransport({jsonTransport: true});
return Promise.resolve(true);
log.info('smtpUrl-empty', { message: 'EmailUtils will log a JSON response instead of sending emails.' })
gTransporter = nodemailer.createTransport({ jsonTransport: true })
return Promise.resolve(true)
}
gTransporter = nodemailer.createTransport(smtpUrl);
const gTransporterVerification = await gTransporter.verify();
gTransporter.use("compile", hbs(hbsOptions));
return Promise.resolve(gTransporterVerification);
gTransporter = nodemailer.createTransport(smtpUrl)
const gTransporterVerification = await gTransporter.verify()
gTransporter.use('compile', hbs(hbsOptions))
return Promise.resolve(gTransporterVerification)
},
sendEmail(aRecipient, aSubject, aTemplate, aContext) {
sendEmail (aRecipient, aSubject, aTemplate, aContext) {
if (!gTransporter) {
return Promise.reject("SMTP transport not initialized");
return Promise.reject('SMTP transport not initialized')
}
const emailContext = Object.assign({
SERVER_URL: AppConstants.SERVER_URL,
}, aContext);
SERVER_URL: AppConstants.SERVER_URL
}, aContext)
return new Promise((resolve, reject) => {
const emailFrom = AppConstants.EMAIL_FROM;
const emailFrom = AppConstants.EMAIL_FROM
const mailOptions = {
from: emailFrom,
to: aRecipient,
@ -62,65 +59,65 @@ const EmailUtils = {
template: aTemplate,
context: emailContext,
headers: {
"x-ses-configuration-set": AppConstants.SES_CONFIG_SET,
},
};
'x-ses-configuration-set': AppConstants.SES_CONFIG_SET
}
}
gTransporter.sendMail(mailOptions, (error, info) => {
if (error) {
reject(error);
return;
reject(error)
return
}
if (gTransporter.transporter.name === "JSONTransport") {
log.info("JSONTransport", { message: info.message.toString() });
if (gTransporter.transporter.name === 'JSONTransport') {
log.info('JSONTransport', { message: info.message.toString() })
}
resolve(info);
});
});
resolve(info)
})
})
},
appendUtmParams(url, campaign, content) {
appendUtmParams (url, campaign, content) {
const utmParameters = {
utm_source: "fx-monitor",
utm_medium: "email",
utm_source: 'fx-monitor',
utm_medium: 'email',
utm_campaign: campaign,
utm_content: content,
};
utm_content: content
}
for (const param in utmParameters) {
url.searchParams.append(param, utmParameters[param]);
url.searchParams.append(param, utmParameters[param])
}
return url;
return url
},
getReportSubject(breaches, req) {
getReportSubject (breaches, req) {
if (breaches.length === 0) {
return req.fluentFormat("email-subject-no-breaches");
return req.fluentFormat('email-subject-no-breaches')
}
return req.fluentFormat("email-subject-found-breaches");
return req.fluentFormat('email-subject-found-breaches')
},
getEmailCtaHref(emailType, campaign, subscriberId=null) {
const subscriberParamPath = (subscriberId) ? `/?subscriber_id=${subscriberId}` : "/";
const url = new URL(subscriberParamPath, AppConstants.SERVER_URL);
return this.appendUtmParams(url, campaign, emailType);
getEmailCtaHref (emailType, campaign, subscriberId = null) {
const subscriberParamPath = (subscriberId) ? `/?subscriber_id=${subscriberId}` : '/'
const url = new URL(subscriberParamPath, AppConstants.SERVER_URL)
return this.appendUtmParams(url, campaign, emailType)
},
getVerificationUrl(subscriber) {
let url = new URL(`${AppConstants.SERVER_URL}/user/verify`);
url = this.appendUtmParams(url, "verified-subscribers", "account-verification-email");
url.searchParams.append("token", encodeURIComponent(subscriber.verification_token));
return url;
getVerificationUrl (subscriber) {
let url = new URL(`${AppConstants.SERVER_URL}/user/verify`)
url = this.appendUtmParams(url, 'verified-subscribers', 'account-verification-email')
url.searchParams.append('token', encodeURIComponent(subscriber.verification_token))
return url
},
getUnsubscribeUrl(subscriber, emailType) {
let url = new URL(`${AppConstants.SERVER_URL}/user/unsubscribe`);
const token = (subscriber.hasOwnProperty("verification_token")) ? subscriber.verification_token : subscriber.primary_verification_token;
const hash = (subscriber.hasOwnProperty("sha1")) ? subscriber.sha1 : subscriber.primary_sha1;
url.searchParams.append("token", encodeURIComponent(token));
url.searchParams.append("hash", encodeURIComponent(hash));
url = this.appendUtmParams(url, "unsubscribe", emailType);
return url;
},
};
getUnsubscribeUrl (subscriber, emailType) {
let url = new URL(`${AppConstants.SERVER_URL}/user/unsubscribe`)
const token = (subscriber.hasOwnProperty('verification_token')) ? subscriber.verification_token : subscriber.primary_verification_token
const hash = (subscriber.hasOwnProperty('sha1')) ? subscriber.sha1 : subscriber.primary_sha1
url.searchParams.append('token', encodeURIComponent(token))
url.searchParams.append('hash', encodeURIComponent(hash))
url = this.appendUtmParams(url, 'unsubscribe', emailType)
return url
}
}
module.exports = EmailUtils;
module.exports = EmailUtils

Просмотреть файл

@ -1,51 +1,51 @@
"use strict";
'use strict'
const { src, watch, series, dest } = require("gulp");
const sass = require("gulp-sass")(require("sass"));
const del = require("del");
const sourcemaps = require("gulp-sourcemaps");
const { src, watch, series, dest } = require('gulp')
const sass = require('gulp-sass')(require('sass'))
const del = require('del')
const sourcemaps = require('gulp-sourcemaps')
// directory for building SCSS, and bundles
const buildDir = "./public/scss/libs/protocol/";
const finalDir = "./public/css/";
const buildDir = './public/scss/libs/protocol/'
const finalDir = './public/css/'
const compiledCssDirectories = [
"./public/css/*",
"!./public/css/legacy/**",
];
'./public/css/*',
'!./public/css/legacy/**'
]
function cleanCompiledCssDirectory() {
return del(compiledCssDirectories);
function cleanCompiledCssDirectory () {
return del(compiledCssDirectories)
}
function resetCssDirectories() {
return del(compiledCssDirectories.concat(buildDir));
function resetCssDirectories () {
return del(compiledCssDirectories.concat(buildDir))
}
function styles() {
return src("./public/scss/app.scss")
.pipe(sourcemaps.init())
.pipe(sass().on("error", sass.logError))
.pipe(sourcemaps.write("."))
.pipe(dest(finalDir));
function styles () {
return src('./public/scss/app.scss')
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(sourcemaps.write('.'))
.pipe(dest(finalDir))
}
function assetsCopyLegacy() {
return src(["./public/scss/partials/legacy/**/*"]).pipe(dest("./public/css/legacy/"));
function assetsCopyLegacy () {
return src(['./public/scss/partials/legacy/**/*']).pipe(dest('./public/css/legacy/'))
}
function watchCss() {
return watch("./public/scss/**/*.scss", { ignoreInitial: false }, series(cleanCompiledCssDirectory, styles));
function watchCss () {
return watch('./public/scss/**/*.scss', { ignoreInitial: false }, series(cleanCompiledCssDirectory, styles))
}
function assetsCopy() {
return src(["./node_modules/@mozilla-protocol/core/protocol/**/*"]).pipe(dest(buildDir));
function assetsCopy () {
return src(['./node_modules/@mozilla-protocol/core/protocol/**/*']).pipe(dest(buildDir))
}
exports.watchCss = watchCss;
exports.watchCss = watchCss
exports.build = series(resetCssDirectories, assetsCopy, assetsCopyLegacy, styles);
exports.build = series(resetCssDirectories, assetsCopy, assetsCopyLegacy, styles)
exports.default = series(
cleanCompiledCssDirectory, assetsCopy, styles, watchCss
);
cleanCompiledCssDirectory, assetsCopy, styles, watchCss
)

204
hibp.js
Просмотреть файл

@ -1,119 +1,115 @@
"use strict";
'use strict'
const got = require("got");
const got = require('got')
const AppConstants = require("./app-constants");
const { FluentError } = require("./locale-utils");
const mozlog = require("./log");
const pkg = require("./package.json");
const AppConstants = require('./app-constants')
const { FluentError } = require('./locale-utils')
const mozlog = require('./log')
const pkg = require('./package.json')
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`;
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`
// When HIBP "re-names" a breach, it keeps its old 'Name' value but gets a new 'Title'
// We use 'Name' in Firefox (via Remote Settings), so we have to maintain our own mapping of re-named breaches.
const RENAMED_BREACHES = ["covve"];
const RENAMED_BREACHES = ['covve']
const RENAMED_BREACHES_MAP = {
"covve": "db8151dd",
};
const log = mozlog("hibp");
covve: 'db8151dd'
}
const log = mozlog('hibp')
const HIBP = {
_addStandardOptions (options = {}) {
const hibpOptions = {
headers: {
"User-Agent": HIBP_USER_AGENT,
'User-Agent': HIBP_USER_AGENT
},
responseType: "json",
};
return Object.assign(options, hibpOptions);
responseType: 'json'
}
return Object.assign(options, hibpOptions)
},
async _throttledGot (url, reqOptions, tryCount = 1) {
let response;
let response
try {
response = await got(url, reqOptions);
return response;
response = await got(url, reqOptions)
return response
} catch (err) {
log.error("_throttledGot", {err: err});
log.error('_throttledGot', { err })
if (err.statusCode === 404) {
// 404 can mean "no results", return undefined response; sorry calling code
return response;
return response
} else if (err.statusCode === 429) {
log.info("_throttledGot", {err: "got a 429, tryCount: " + tryCount});
log.info('_throttledGot', { err: 'got a 429, tryCount: ' + tryCount })
if (tryCount >= AppConstants.HIBP_THROTTLE_MAX_TRIES) {
log.error("_throttledGot", {err: err});
throw new FluentError("error-hibp-throttled");
log.error('_throttledGot', { err })
throw new FluentError('error-hibp-throttled')
} else {
tryCount++;
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount));
return await this._throttledGot(url, reqOptions, tryCount);
tryCount++
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount))
return await this._throttledGot(url, reqOptions, tryCount)
}
} else {
throw new FluentError("error-hibp-connect");
throw new FluentError('error-hibp-connect')
}
}
},
async req(path, options = {}) {
const url = `${AppConstants.HIBP_API_ROOT}${path}`;
const reqOptions = this._addStandardOptions(options);
return await this._throttledGot(url, reqOptions);
async req (path, options = {}) {
const url = `${AppConstants.HIBP_API_ROOT}${path}`
const reqOptions = this._addStandardOptions(options)
return await this._throttledGot(url, reqOptions)
},
async kAnonReq(path, options = {}) {
async kAnonReq (path, options = {}) {
// Construct HIBP url and standard headers
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`;
const reqOptions = this._addStandardOptions(options);
return await this._throttledGot(url, reqOptions);
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`
const reqOptions = this._addStandardOptions(options)
return await this._throttledGot(url, reqOptions)
},
matchFluentID(dataCategory) {
matchFluentID (dataCategory) {
return dataCategory.toLowerCase()
.replace(/[^-a-z0-9]/g, "-")
.replace(/-{2,}/g, "-")
.replace(/(^-|-$)/g, "");
.replace(/[^-a-z0-9]/g, '-')
.replace(/-{2,}/g, '-')
.replace(/(^-|-$)/g, '')
},
formatDataClassesArray(dataCategories) {
const formattedArray = [];
dataCategories.forEach(category => {
formattedArray.push(this.matchFluentID(category));
});
return formattedArray;
formatDataClassesArray (dataCategories) {
const formattedArray = []
dataCategories.forEach(category => {
formattedArray.push(this.matchFluentID(category))
})
return formattedArray
},
async loadBreachesIntoApp(app) {
log.info("loadBreachesIntoApp");
async loadBreachesIntoApp (app) {
log.info('loadBreachesIntoApp')
try {
const breachesResponse = await this.req("/breaches");
const breaches = [];
const breachesResponse = await this.req('/breaches')
const breaches = []
for (const breach of breachesResponse.body) {
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses);
breach.LogoPath = /[^/]*$/.exec(breach.LogoPath)[0];
breaches.push(breach);
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses)
breach.LogoPath = /[^/]*$/.exec(breach.LogoPath)[0]
breaches.push(breach)
}
app.locals.breaches = breaches;
app.locals.breachesLoadedDateTime = Date.now();
app.locals.latestBreach = this.getLatestBreach(breaches);
app.locals.mostRecentBreachDateTime = app.locals.latestBreach.AddedDate;
app.locals.breaches = breaches
app.locals.breachesLoadedDateTime = Date.now()
app.locals.latestBreach = this.getLatestBreach(breaches)
app.locals.mostRecentBreachDateTime = app.locals.latestBreach.AddedDate
} catch (error) {
throw new FluentError("error-hibp-load-breaches");
throw new FluentError('error-hibp-load-breaches')
}
log.info("done-loading-breaches");
log.info('done-loading-breaches')
},
async getBreachesForEmail (sha1, allBreaches, includeSensitive = false, filterBreaches = true) {
let foundBreaches = []
const sha1Prefix = sha1.slice(0, 6).toUpperCase()
const path = `/breachedaccount/range/${sha1Prefix}`
async getBreachesForEmail(sha1, allBreaches, includeSensitive = false, filterBreaches = true) {
let foundBreaches = [];
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
const path = `/breachedaccount/range/${sha1Prefix}`;
const response = await this.kAnonReq(path);
const response = await this.kAnonReq(path)
if (!response) {
return [];
return []
}
// Parse response body, format:
// [
@ -122,79 +118,75 @@ const HIBP = {
// ]
for (const breachedAccount of response.body) {
if (sha1.toUpperCase() === sha1Prefix + breachedAccount.hashSuffix) {
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name));
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name))
if (filterBreaches) {
foundBreaches = this.filterBreaches(foundBreaches);
foundBreaches = this.filterBreaches(foundBreaches)
}
// NOTE: DO NOT CHANGE THIS SORT LOGIC
// We store breach resolutions by recency indices,
// so that our DB does not contain any part of any user's list of accounts
foundBreaches.sort( (a,b) => {
return new Date(b.AddedDate) - new Date(a.AddedDate);
});
foundBreaches.sort((a, b) => {
return new Date(b.AddedDate) - new Date(a.AddedDate)
})
break;
break
}
}
if (includeSensitive) {
return foundBreaches;
return foundBreaches
}
return foundBreaches.filter(
breach => !breach.IsSensitive
);
)
},
getBreachByName(allBreaches, breachName) {
breachName = breachName.toLowerCase();
getBreachByName (allBreaches, breachName) {
breachName = breachName.toLowerCase()
if (RENAMED_BREACHES.includes(breachName)) {
breachName = RENAMED_BREACHES_MAP[breachName];
breachName = RENAMED_BREACHES_MAP[breachName]
}
const foundBreach = allBreaches.find(breach => breach.Name.toLowerCase() === breachName);
return foundBreach;
const foundBreach = allBreaches.find(breach => breach.Name.toLowerCase() === breachName)
return foundBreach
},
filterBreaches(breaches) {
filterBreaches (breaches) {
return breaches.filter(
breach => !breach.IsRetired &&
!breach.IsSpamList &&
!breach.IsFabricated &&
breach.IsVerified &&
breach.Domain !== ""
);
breach.Domain !== ''
)
},
getLatestBreach(breaches) {
let latestBreach = {};
let latestBreachDateTime = new Date(0);
getLatestBreach (breaches) {
let latestBreach = {}
let latestBreachDateTime = new Date(0)
for (const breach of breaches) {
if (breach.IsSensitive) {
continue;
continue
}
const breachAddedDate = new Date(breach.AddedDate);
const breachAddedDate = new Date(breach.AddedDate)
if (breachAddedDate > latestBreachDateTime) {
latestBreachDateTime = breachAddedDate;
latestBreach = breach;
latestBreachDateTime = breachAddedDate
latestBreach = breach
}
}
return latestBreach;
return latestBreach
},
async subscribeHash(sha1) {
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
const path = "/range/subscribe";
async subscribeHash (sha1) {
const sha1Prefix = sha1.slice(0, 6).toUpperCase()
const path = '/range/subscribe'
const options = {
method: "POST",
json: {hashPrefix: sha1Prefix},
};
method: 'POST',
json: { hashPrefix: sha1Prefix }
}
return await this.kAnonReq(path, options);
},
};
return await this.kAnonReq(path, options)
}
}
module.exports = HIBP;
module.exports = HIBP

Просмотреть файл

@ -1,54 +1,54 @@
"use strict";
'use strict'
const path = require("path");
const reader = require("@maxmind/geoip2-node").Reader;
const AppConstants = require("./app-constants");
const path = require('path')
const reader = require('@maxmind/geoip2-node').Reader
const AppConstants = require('./app-constants')
let locationDb, timestamp;
let locationDb, timestamp
async function openLocationDb() {
if (locationDb && isFresh()) return console.warn("Location database already open.");
async function openLocationDb () {
if (locationDb && isFresh()) return console.warn('Location database already open.')
try {
const dbPath = path.join(AppConstants.GEOIP_GEOLITE2_PATH, AppConstants.GEOIP_GEOLITE2_CITY_FILENAME);
locationDb = await reader.open(dbPath);
const dbPath = path.join(AppConstants.GEOIP_GEOLITE2_PATH, AppConstants.GEOIP_GEOLITE2_CITY_FILENAME)
locationDb = await reader.open(dbPath)
} catch (e) {
return console.warn("Could not open location database:", e.message);
return console.warn('Could not open location database:', e.message)
}
timestamp = Date.now();
return true;
timestamp = Date.now()
return true
}
async function readLocationData(ip, locales) {
let locationArr;
async function readLocationData (ip, locales) {
let locationArr
if (!isFresh()) await openLocationDb();
if (!isFresh()) await openLocationDb()
try {
const data = locationDb.city(ip);
const countryName = data.country?.names[locales.find(locale => data.country?.names[locale])]; // find valid locale key and return its value
const cityName = data.city?.names[locales.find(locale => data.city?.names[locale])];
const subdivisionName = data.subdivisions?.[0].isoCode;
const subdivisionFiltered = /[A-z]{2,}/.test(subdivisionName) ? subdivisionName : null; // return strings that are 2 or more letters, or null (avoid unfamiliar subdivisions like `E` or `09`)
const data = locationDb.city(ip)
const countryName = data.country?.names[locales.find(locale => data.country?.names[locale])] // find valid locale key and return its value
const cityName = data.city?.names[locales.find(locale => data.city?.names[locale])]
const subdivisionName = data.subdivisions?.[0].isoCode
const subdivisionFiltered = /[A-z]{2,}/.test(subdivisionName) ? subdivisionName : null // return strings that are 2 or more letters, or null (avoid unfamiliar subdivisions like `E` or `09`)
locationArr = [cityName, subdivisionFiltered, countryName].filter(str => str); // [city name, state code, country code] with non-null items.
locationArr = [cityName, subdivisionFiltered, countryName].filter(str => str) // [city name, state code, country code] with non-null items.
} catch (e) {
return console.warn("Could not read location from database:", e.message);
return console.warn('Could not read location from database:', e.message)
}
return {
shortLocation: locationArr.slice(0, 2).join(", "), // shows the first two location values from the ones available
fullLocation: locationArr.join(", "), // shows up to three location values from the ones available
};
shortLocation: locationArr.slice(0, 2).join(', '), // shows the first two location values from the ones available
fullLocation: locationArr.join(', ') // shows up to three location values from the ones available
}
}
function isFresh() {
if (Date.now() - timestamp < 259200000) return true; // 1000ms * 60s * 60m * 24h * 3 elapsed time is less than 24hrs
return false;
function isFresh () {
if (Date.now() - timestamp < 259200000) return true // 1000ms * 60s * 60m * 24h * 3 elapsed time is less than 24hrs
return false
}
module.exports = {
openLocationDb,
readLocationData,
};
readLocationData
}

Просмотреть файл

@ -1,18 +1,18 @@
"use strict";
'use strict'
const changePWLinks = {
"MySpace": "https://myspace.com/settings/profile/password",
"LinkedIn": "https://www.linkedin.com/psettings/change-password",
"Dubsmash": "https://dubsmash.com/reset-password",
"Canva": "https://www.canva.com/account",
"MyFitnessPal": "https://www.myfitnesspal.com/account/change_password",
"Adobe": "https://account.adobe.com/security",
"Dropbox": "https://www.dropbox.com/account/security",
"Houzz": "https://www.houzz.com/changePassword",
"Evite": "https://www.evite.com/settings/password",
"CafePress": "https://members.cafepress.com/account/profile.aspx",
};
MySpace: 'https://myspace.com/settings/profile/password',
LinkedIn: 'https://www.linkedin.com/psettings/change-password',
Dubsmash: 'https://dubsmash.com/reset-password',
Canva: 'https://www.canva.com/account',
MyFitnessPal: 'https://www.myfitnesspal.com/account/change_password',
Adobe: 'https://account.adobe.com/security',
Dropbox: 'https://www.dropbox.com/account/security',
Houzz: 'https://www.houzz.com/changePassword',
Evite: 'https://www.evite.com/settings/password',
CafePress: 'https://members.cafepress.com/account/profile.aspx'
}
module.exports = {
changePWLinks,
};
changePWLinks
}

Просмотреть файл

@ -1,110 +1,108 @@
"use strict";
'use strict'
const ClientOAuth2 = require("client-oauth2");
const got = require("got");
const { URL } = require("url");
const ClientOAuth2 = require('client-oauth2')
const got = require('got')
const { URL } = require('url')
const AppConstants = require("../app-constants");
const mozlog = require("../log");
const AppConstants = require('../app-constants')
const mozlog = require('../log')
const log = mozlog("fxa");
const log = mozlog('fxa')
// This object exists instead of inlining the env vars to make it easy
// to abstract fetching API endpoints from the OAuth server (instead
// of specifying them in the environment) in the future.
const FxAOAuthUtils = {
get authorizationUri() { return AppConstants.OAUTH_AUTHORIZATION_URI; },
get tokenUri() { return AppConstants.OAUTH_TOKEN_URI; },
get profileUri() { return AppConstants.OAUTH_PROFILE_URI; },
};
get authorizationUri () { return AppConstants.OAUTH_AUTHORIZATION_URI },
get tokenUri () { return AppConstants.OAUTH_TOKEN_URI },
get profileUri () { return AppConstants.OAUTH_PROFILE_URI }
}
const FxAOAuthClient = new ClientOAuth2({
clientId: AppConstants.OAUTH_CLIENT_ID,
clientSecret: AppConstants.OAUTH_CLIENT_SECRET,
accessTokenUri: FxAOAuthUtils.tokenUri,
authorizationUri: FxAOAuthUtils.authorizationUri,
redirectUri: AppConstants.SERVER_URL + "/oauth/confirmed",
scopes: ["profile"],
});
redirectUri: AppConstants.SERVER_URL + '/oauth/confirmed',
scopes: ['profile']
})
const FXA = {
async _postTokenRequest(path, token) {
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin;
const tokenUrl = `${fxaTokenOrigin}${path}`;
const tokenBody = (typeof token === "object") ? token : {token};
async _postTokenRequest (path, token) {
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin
const tokenUrl = `${fxaTokenOrigin}${path}`
const tokenBody = (typeof token === 'object') ? token : { token }
const tokenOptions = {
method: "POST",
method: 'POST',
json: tokenBody,
responseType: "json",
};
responseType: 'json'
}
try {
const response = await got(tokenUrl, tokenOptions);
return response;
const response = await got(tokenUrl, tokenOptions)
return response
} catch (e) {
log.error("_postTokenRequest", {stack: e.stack});
return e;
log.error('_postTokenRequest', { stack: e.stack })
return e
}
},
async verifyOAuthToken(token) {
async verifyOAuthToken (token) {
try {
const response = await this._postTokenRequest("/v1/verify", token);
return response;
const response = await this._postTokenRequest('/v1/verify', token)
return response
} catch (e) {
log.error("verifyOAuthToken", {stack: e.stack});
log.error('verifyOAuthToken', { stack: e.stack })
}
},
async destroyOAuthToken(token) {
async destroyOAuthToken (token) {
try {
const response = await this._postTokenRequest("/v1/destroy", token);
return response;
const response = await this._postTokenRequest('/v1/destroy', token)
return response
} catch (e) {
log.error("destroyOAuthToken", {stack: e.stack});
log.error('destroyOAuthToken', { stack: e.stack })
}
},
async revokeOAuthTokens(subscriber) {
await this.destroyOAuthToken({ token: subscriber.fxa_access_token });
await this.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token });
async revokeOAuthTokens (subscriber) {
await this.destroyOAuthToken({ token: subscriber.fxa_access_token })
await this.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token })
},
async getProfileData(accessToken) {
async getProfileData (accessToken) {
try {
const data = await got(FxAOAuthUtils.profileUri,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
return data.body;
)
return data.body
} catch (e) {
log.warn("getProfileData", {stack: e.stack});
return e;
log.warn('getProfileData', { stack: e.stack })
return e
}
},
async sendMetricsFlowPing(path) {
const fxaMetricsFlowUrl = new URL(path, AppConstants.FXA_SETTINGS_URL);
async sendMetricsFlowPing (path) {
const fxaMetricsFlowUrl = new URL(path, AppConstants.FXA_SETTINGS_URL)
const fxaMetricsFlowOptions = {
method: "GET",
headers: { Origin: AppConstants.SERVER_URL },
};
try {
log.info(`GETting ${fxaMetricsFlowUrl}, with options: ${JSON.stringify(fxaMetricsFlowOptions)}`);
const fxaResp = await got(fxaMetricsFlowUrl, fxaMetricsFlowOptions);
log.info("pinged FXA metrics flow.");
return fxaResp;
} catch (e) {
log.error("sendMetricsFlowPing", {stack: e.stack});
return false;
method: 'GET',
headers: { Origin: AppConstants.SERVER_URL }
}
},
};
try {
log.info(`GETting ${fxaMetricsFlowUrl}, with options: ${JSON.stringify(fxaMetricsFlowOptions)}`)
const fxaResp = await got(fxaMetricsFlowUrl, fxaMetricsFlowOptions)
log.info('pinged FXA metrics flow.')
return fxaResp
} catch (e) {
log.error('sendMetricsFlowPing', { stack: e.stack })
return false
}
}
}
module.exports = {
FXA,
FxAOAuthClient,
};
FxAOAuthClient
}

Просмотреть файл

@ -1,49 +1,47 @@
"use strict";
'use strict'
const got = require("got");
const AppConstants = require("../app-constants");
const got = require('got')
const AppConstants = require('../app-constants')
const BREACHES_COLLECTION = "fxmonitor-breaches";
const FX_RS_COLLECTION = `${AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`;
const FX_RS_RECORDS = `${FX_RS_COLLECTION}/records`;
const FX_RS_WRITER_USER = AppConstants.FX_REMOTE_SETTINGS_WRITER_USER;
const FX_RS_WRITER_PASS = AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS;
const BREACHES_COLLECTION = 'fxmonitor-breaches'
const FX_RS_COLLECTION = `${AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`
const FX_RS_RECORDS = `${FX_RS_COLLECTION}/records`
const FX_RS_WRITER_USER = AppConstants.FX_REMOTE_SETTINGS_WRITER_USER
const FX_RS_WRITER_PASS = AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS
const RemoteSettings = {
async whichBreachesAreNotInRemoteSettingsYet(breaches) {
async whichBreachesAreNotInRemoteSettingsYet (breaches) {
const fxRSRecords = await got(FX_RS_RECORDS, {
responseType: "json",
responseType: 'json',
username: FX_RS_WRITER_USER,
password: FX_RS_WRITER_PASS,
});
password: FX_RS_WRITER_PASS
})
const remoteSettingsBreachesSet = new Set(
fxRSRecords.body.data.map(b => b.Name)
);
)
return breaches.filter( ({Name}) => !remoteSettingsBreachesSet.has(Name) );
return breaches.filter(({ Name }) => !remoteSettingsBreachesSet.has(Name))
},
async postNewBreachToBreachesCollection(data) {
async postNewBreachToBreachesCollection (data) {
// Create the record
return await got.post(FX_RS_RECORDS, {
username: FX_RS_WRITER_USER,
password: FX_RS_WRITER_PASS,
json: { data },
responseType: "json",
});
responseType: 'json'
})
},
async requestReviewOnBreachesCollection() {
async requestReviewOnBreachesCollection () {
return await got.patch(FX_RS_COLLECTION, {
username: FX_RS_WRITER_USER,
password: FX_RS_WRITER_PASS,
json: { data: {status: "to-review"} },
responseType: "json",
});
},
};
json: { data: { status: 'to-review' } },
responseType: 'json'
})
}
}
module.exports = RemoteSettings;
module.exports = RemoteSettings

Просмотреть файл

@ -1,109 +1,106 @@
"use strict";
'use strict'
const fs = require("fs");
const path = require("path");
const fs = require('fs')
const path = require('path')
// node.js needs Intl.PluralRules polyfill
require("intl-pluralrules");
require('intl-pluralrules')
const { FluentBundle } = require("fluent");
const { FluentBundle } = require('fluent')
const mozlog = require("./log");
const {supportedLocales} = require("./package.json");
const mozlog = require('./log')
const { supportedLocales } = require('./package.json')
const log = mozlog('locale-utils')
const log = mozlog("locale-utils");
const localesDir = "locales";
const availableLanguages = [];
const fluentBundles = {};
const localesDir = 'locales'
const availableLanguages = []
const fluentBundles = {}
class FluentError extends Error {
constructor(fluentID = null, ...params) {
super(...params);
constructor (fluentID = null, ...params) {
super(...params)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FluentError);
Error.captureStackTrace(this, FluentError)
}
this.fluentID = fluentID;
this.message = fluentID;
this.fluentID = fluentID
this.message = fluentID
}
}
const LocaleUtils = {
init() {
let languageDirectories = supportedLocales.split(",");
if (supportedLocales === "*") {
languageDirectories = fs.readdirSync( localesDir ).filter(item => {
return (!item.startsWith(".") && fs.lstatSync(path.join(localesDir, item)).isDirectory());
});
init () {
let languageDirectories = supportedLocales.split(',')
if (supportedLocales === '*') {
languageDirectories = fs.readdirSync(localesDir).filter(item => {
return (!item.startsWith('.') && fs.lstatSync(path.join(localesDir, item)).isDirectory())
})
}
for (const lang of languageDirectories) {
try {
const langBundle = new FluentBundle(lang, {useIsolating: false});
const langBundle = new FluentBundle(lang, { useIsolating: false })
const ftlFiles = fs.readdirSync(path.join(localesDir, lang)).filter(item => {
return (item.endsWith(".ftl"));
});
return (item.endsWith('.ftl'))
})
for (const file of ftlFiles) {
const langFTLSource = fs.readFileSync(path.join(localesDir, lang, file), "utf8");
langBundle.addMessages(langFTLSource);
const langFTLSource = fs.readFileSync(path.join(localesDir, lang, file), 'utf8')
langBundle.addMessages(langFTLSource)
}
fluentBundles[lang] = langBundle;
availableLanguages.push(lang);
fluentBundles[lang] = langBundle
availableLanguages.push(lang)
} catch (e) {
log.error("loadFluentBundle", {stack: e.stack});
log.error('loadFluentBundle', { stack: e.stack })
}
}
log.info("LocaleUtils.init", {availableLanguages});
log.info("LocaleUtils.init", {fluentBundles});
return {availableLanguages, fluentBundles};
log.info('LocaleUtils.init', { availableLanguages })
log.info('LocaleUtils.init', { fluentBundles })
return { availableLanguages, fluentBundles }
},
loadLanguagesIntoApp (app) {
app.locals.AVAILABLE_LANGUAGES = availableLanguages;
app.locals.FLUENT_BUNDLES = fluentBundles;
app.locals.AVAILABLE_LANGUAGES = availableLanguages
app.locals.FLUENT_BUNDLES = fluentBundles
},
fluentFormat (supportedLocales, id, args=null, errors=null) {
fluentFormat (supportedLocales, id, args = null, errors = null) {
for (const locale of supportedLocales) {
const bundle = fluentBundles[locale];
const bundle = fluentBundles[locale]
if (bundle.hasMessage(id)) {
const message = bundle.getMessage(id);
return bundle.format(message, args);
const message = bundle.getMessage(id)
return bundle.format(message, args)
}
}
return id;
return id
},
fluentFormatWithFallback (supportedLocales, id, fallbackId, args=null, errors=null) {
fluentFormatWithFallback (supportedLocales, id, fallbackId, args = null, errors = null) {
if (!fallbackId) {
log.error("fluentFormatWithFallback: No fallbackId");
return false;
log.error('fluentFormatWithFallback: No fallbackId')
return false
}
for (const locale of supportedLocales) {
const bundle = fluentBundles[locale];
const bundle = fluentBundles[locale]
if (bundle.hasMessage(id)) {
const message = bundle.getMessage(id);
return bundle.format(message, args);
const message = bundle.getMessage(id)
return bundle.format(message, args)
}
// If first message id doesn't have translation, use fallback id
if (fallbackId && bundle.hasMessage(fallbackId)) {
const message = bundle.getMessage(fallbackId);
return bundle.format(message, args);
const message = bundle.getMessage(fallbackId)
return bundle.format(message, args)
}
}
return id;
},
return id
}
};
}
module.exports = {
FluentError,
LocaleUtils,
};
LocaleUtils
}

15
log.js
Просмотреть файл

@ -1,14 +1,13 @@
"use strict";
'use strict'
const mozlog = require("mozlog");
const mozlog = require('mozlog')
const AppConstants = require("./app-constants");
const AppConstants = require('./app-constants')
const log = mozlog({
app: "fx-monitor",
app: 'fx-monitor',
level: AppConstants.MOZLOG_LEVEL,
fmt: AppConstants.MOZLOG_FMT,
});
fmt: AppConstants.MOZLOG_FMT
})
module.exports = log;
module.exports = log

Просмотреть файл

@ -1,247 +1,234 @@
"use strict";
'use strict'
const { URLSearchParams } = require("url");
const { URLSearchParams } = require('url')
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
const Sentry = require("@sentry/node");
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
const Sentry = require('@sentry/node')
const AppConstants = require("./app-constants");
const DB = require("./db/DB");
const { FXA } = require("./lib/fxa");
const { FluentError } = require("./locale-utils");
const mozlog = require("./log");
const AppConstants = require('./app-constants')
const DB = require('./db/DB')
const { FXA } = require('./lib/fxa')
const { FluentError } = require('./locale-utils')
const mozlog = require('./log')
const HIBP = require("./hibp");
const log = mozlog("middleware");
const HIBP = require('./hibp')
const log = mozlog('middleware')
// adds the request object to a res.local var
function addRequestToResponse (req, res, next) {
res.locals.req = req;
next();
res.locals.req = req
next()
}
// picks available language by Accept-Language and assigns to request
function pickLanguage (req, res, next) {
res.vary("Accept-Language");
const requestedLanguage = acceptedLanguages(req.headers["accept-language"]);
res.vary('Accept-Language')
const requestedLanguage = acceptedLanguages(req.headers['accept-language'])
const supportedLocales = negotiateLanguages(
requestedLanguage,
req.app.locals.AVAILABLE_LANGUAGES,
{defaultLocale: "en"}
);
req.supportedLocales = supportedLocales;
{ defaultLocale: 'en' }
)
req.supportedLocales = supportedLocales
req.fluentFormat = (id, args = null, errors = null) => {
for (const locale of supportedLocales) {
const bundle = req.app.locals.FLUENT_BUNDLES[locale];
const bundle = req.app.locals.FLUENT_BUNDLES[locale]
if (bundle.hasMessage(id)) {
const message = bundle.getMessage(id);
return bundle.format(message, args);
const message = bundle.getMessage(id)
return bundle.format(message, args)
}
}
return id;
};
return id
}
next();
next()
}
async function recordVisitFromEmail (req, res, next) {
if (req.query.utm_source && req.query.utm_source !== "fx-monitor") {
next();
return;
if (req.query.utm_source && req.query.utm_source !== 'fx-monitor') {
next()
return
}
if (req.query.utm_medium && req.query.utm_medium !== "email") {
next();
return;
if (req.query.utm_medium && req.query.utm_medium !== 'email') {
next()
return
}
const breachDetailsRE = /breach-details\/(\w*)$/;
const capturedMatch = req.path.match(breachDetailsRE);
const breachDetailsRE = /breach-details\/(\w*)$/
const capturedMatch = req.path.match(breachDetailsRE)
// Send engagement ping to FXA if req.query contains a valid subscriber ID
if (req.query.subscriber_id && Number.isInteger(Number(req.query.subscriber_id))) {
const subscriber = await DB.getSubscriberById(req.query.subscriber_id);
const subscriber = await DB.getSubscriberById(req.query.subscriber_id)
if (!subscriber.fxa_uid || subscriber.fxa_uid === "") {
next();
return;
if (!subscriber.fxa_uid || subscriber.fxa_uid === '') {
next()
return
}
const utmContent = (capturedMatch) ? `&utm_content=${capturedMatch[1]}` : "";
const fxaMetricsFlowPath = `metrics-flow?utm_source=${req.query.utm_source}&utm_medium=${req.query.utm_medium}${utmContent}&event_type=engage&uid=${subscriber.fxa_uid}&service=${AppConstants.OAUTH_CLIENT_ID}`;
const fxaResult = await FXA.sendMetricsFlowPing(fxaMetricsFlowPath);
log.info(`fxaResult: ${fxaResult}`);
const utmContent = (capturedMatch) ? `&utm_content=${capturedMatch[1]}` : ''
const fxaMetricsFlowPath = `metrics-flow?utm_source=${req.query.utm_source}&utm_medium=${req.query.utm_medium}${utmContent}&event_type=engage&uid=${subscriber.fxa_uid}&service=${AppConstants.OAUTH_CLIENT_ID}`
const fxaResult = await FXA.sendMetricsFlowPing(fxaMetricsFlowPath)
log.info(`fxaResult: ${fxaResult}`)
}
// If user is already signed in, proceed
if (req.session.user) {
next();
return;
next()
return
}
// If user is returning from FXA sign-in, proceed
if (req.path === "/oauth/confirmed") {
next();
return;
if (req.path === '/oauth/confirmed') {
next()
return
}
// Redirect users who have clicked "Go to Dashboard" from an email and aren't signed in to the /oauth flow.
if (
req.query.utm_campaign && req.query.utm_campaign === "go-to-dashboard-link"
) {
const oauthUrl = new URL("/oauth/init", AppConstants.SERVER_URL);
["utm_source", "utm_campaign", "utm_medium"].forEach(param => {
req.query.utm_campaign && req.query.utm_campaign === 'go-to-dashboard-link'
) {
const oauthUrl = new URL('/oauth/init', AppConstants.SERVER_URL);
['utm_source', 'utm_campaign', 'utm_medium'].forEach(param => {
if (req.query[param]) {
oauthUrl.searchParams.append(param, req.query[param]);
oauthUrl.searchParams.append(param, req.query[param])
}
});
req.url = `${oauthUrl.pathname}/${oauthUrl.search}`;
next();
return;
})
req.url = `${oauthUrl.pathname}/${oauthUrl.search}`
next()
return
}
next();
next()
}
// Helps handle errors for all async route controllers
// See https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
function asyncMiddleware (fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
Promise.resolve(fn(req, res, next)).catch(next)
}
}
function logErrors (err, req, res, next) {
log.error("error", {stack: err.stack});
Sentry.captureException(err);
next(err);
log.error('error', { stack: err.stack })
Sentry.captureException(err)
next(err)
}
function localizeErrorMessages (err, req, res, next) {
if (err instanceof FluentError) {
err.message = req.fluentFormat(err.fluentID);
err.locales = req.supportedLocales;
err.message = req.fluentFormat(err.fluentID)
err.locales = req.supportedLocales
}
next(err);
next(err)
}
function clientErrorHandler (err, req, res, next) {
if (req.xhr || req.headers["content-type"] === "application/json") {
res.status(500).send({ message: err.message });
if (req.xhr || req.headers['content-type'] === 'application/json') {
res.status(500).send({ message: err.message })
} else {
next(err);
next(err)
}
}
function errorHandler (err, req, res, next) {
res.status(500);
res.render("subpage", {
analyticsID: "error",
headline: req.fluentFormat("error-headline"),
subhead: err.message,
});
res.status(500)
res.render('subpage', {
analyticsID: 'error',
headline: req.fluentFormat('error-headline'),
subhead: err.message
})
}
async function _getRequestSessionUser(req, res, next) {
async function _getRequestSessionUser (req, res, next) {
if (req.session && req.session.user) {
// make sure the user object has all subscribers and email_addresses properties
return DB.getSubscriberById(req.session.user.id);
return DB.getSubscriberById(req.session.user.id)
}
return null;
return null
}
async function requireSessionUser(req, res, next) {
const user = await _getRequestSessionUser(req);
async function requireSessionUser (req, res, next) {
const user = await _getRequestSessionUser(req)
if (!user) {
const queryParams = new URLSearchParams(req.query).toString();
return res.redirect(`/oauth/init?${queryParams}`);
const queryParams = new URLSearchParams(req.query).toString()
return res.redirect(`/oauth/init?${queryParams}`)
}
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token);
if (fxaProfileData.hasOwnProperty("name") && fxaProfileData.name === "HTTPError") {
delete req.session.user;
return res.redirect("/");
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token)
if (fxaProfileData.hasOwnProperty('name') && fxaProfileData.name === 'HTTPError') {
delete req.session.user
return res.redirect('/')
}
await DB.updateFxAProfileData(user, fxaProfileData);
req.session.user = user;
req.user = user;
next();
await DB.updateFxAProfileData(user, fxaProfileData)
req.session.user = user
req.user = user
next()
}
function getShareUTMs(req, res, next) {
function getShareUTMs (req, res, next) {
// Step 1: See if the user needs to be redirected to the homepage or to a breach-detail page.
const generalShareUrls = [
"/share/orange", //Header
"/share/purple", // Footer
"/share/blue", // user/dashboard
"/share/",
];
'/share/orange', // Header
'/share/purple', // Footer
'/share/blue', // user/dashboard
'/share/'
]
if (generalShareUrls.includes(req.url)) {
// If not breach specific, redirect to "/"
req.session.redirectHome = true;
req.session.redirectHome = true
}
// If user has no reference to experiment (default), add skip override
if (typeof(req.session.experimentFlags) === "undefined") {
if (typeof (req.session.experimentFlags) === 'undefined') {
req.session.experimentFlags = {
excludeFromExperiment: true,
};
excludeFromExperiment: true
}
}
const excludedFromExperiment = (req.session.experimentFlags.excludeFromExperiment);
const excludedFromExperiment = (req.session.experimentFlags.excludeFromExperiment)
// Excluse user from experiment if they don't have any experimentFlags set already.
if (excludedFromExperiment) {
// Step 2: Determine if user needs to have share-link UTMs set
const colors = [
"orange", //Header
"purple", // Footer
"blue", // user/dashboard
];
const urlArray = req.url.split("/");
const color = urlArray.slice(-1)[0];
'orange', // Header
'purple', // Footer
'blue' // user/dashboard
]
const urlArray = req.url.split('/')
const color = urlArray.slice(-1)[0]
req.session.utmOverrides = {
campaignName: "shareLinkTraffic",
campaignTerm: "default",
};
campaignName: 'shareLinkTraffic',
campaignTerm: 'default'
}
// Set Color Var in UTM
if (color.length && colors.includes(color)) {
req.session.utmOverrides.campaignTerm = color;
req.session.utmOverrides.campaignTerm = color
}
if (color.length && !colors.includes(color)) {
const allBreaches = req.app.locals.breaches;
const breachName = color;
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName);
const allBreaches = req.app.locals.breaches
const breachName = color
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName)
if (featuredBreach) {
req.session.utmOverrides.campaignTerm = featuredBreach.Name;
req.session.utmOverrides.campaignTerm = featuredBreach.Name
}
}
// Exclude share users
req.session.experimentFlags = {
excludeFromExperiment: true,
};
excludeFromExperiment: true
}
}
next();
next()
}
module.exports = {
addRequestToResponse,
pickLanguage,
@ -252,5 +239,5 @@ module.exports = {
clientErrorHandler,
errorHandler,
requireSessionUser,
getShareUTMs,
};
getShareUTMs
}

Просмотреть файл

@ -1,205 +1,205 @@
"use strict";
'use strict'
/* global sendPing */
function replaceLogo(e) {
e.target.src = "/img/logos/missing-logo-icon.png";
e.target.removeEventListener("error", replaceLogo);
return true;
function replaceLogo (e) {
e.target.src = '/img/logos/missing-logo-icon.png'
e.target.removeEventListener('error', replaceLogo)
return true
}
function breachImages() {
this.active = false;
function breachImages () {
this.active = false
this.lazyLoad = () => {
const lazyImages = [].slice.call(document.querySelectorAll(".lazy-img"));
const lazyImages = [].slice.call(document.querySelectorAll('.lazy-img'))
if (!this.active) {
this.active = true;
const winHeight = window.innerHeight;
this.active = true
const winHeight = window.innerHeight
lazyImages.forEach(lazyImage => {
if ((lazyImage.getBoundingClientRect().top <= winHeight && lazyImage.getBoundingClientRect().bottom >= 0)) {
lazyImage.classList.add("lazy-loaded");
lazyImage.classList.remove("lazy-img");
lazyImage.src = lazyImage.dataset.src;
lazyImage.addEventListener("error", replaceLogo);
lazyImage.classList.add('lazy-loaded')
lazyImage.classList.remove('lazy-img')
lazyImage.src = lazyImage.dataset.src
lazyImage.addEventListener('error', replaceLogo)
if (lazyImages.length === 0) {
document.removeEventListener("scroll", this.lazyLoad);
window.removeEventListener("resize", this.lazyLoad);
window.removeEventListener("orientationchange", this.lazyLoad);
document.removeEventListener('scroll', this.lazyLoad)
window.removeEventListener('resize', this.lazyLoad)
window.removeEventListener('orientationchange', this.lazyLoad)
}
}
}),
this.active = false;
this.active = false
}
};
}
}
const makeBreachInfoSpans = (className, spanContent, wrapper) => {
const span = document.createElement("span");
span["classList"] = className;
span.textContent = spanContent;
wrapper.appendChild(span);
return span;
};
function makeDiv(className, wrapper) {
const div = document.createElement("div");
div["classList"] = className;
wrapper.appendChild(div);
return div;
const span = document.createElement('span')
span.classList = className
span.textContent = spanContent
wrapper.appendChild(span)
return span
}
function clearBreaches(wrapper) {
function makeDiv (className, wrapper) {
const div = document.createElement('div')
div.classList = className
wrapper.appendChild(div)
return div
}
function clearBreaches (wrapper) {
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild);
wrapper.removeChild(wrapper.firstChild)
}
}
function makeBreaches(breaches, LocalizedBreachCardStrings, breachCardWrapper, breachLogos) {
breachCardWrapper.classList.toggle("hide-breaches");
clearBreaches(breachCardWrapper);
function makeBreaches (breaches, LocalizedBreachCardStrings, breachCardWrapper, breachLogos) {
breachCardWrapper.classList.toggle('hide-breaches')
clearBreaches(breachCardWrapper)
const fragment = document.createDocumentFragment();
fragment["id"] = "all-breaches";
const fragment = document.createDocumentFragment()
fragment.id = 'all-breaches'
const logosOrigin = document.body.dataset.logosOrigin;
const logosOrigin = document.body.dataset.logosOrigin
for (const breach of breaches) {
const card = document.createElement("a");
const card = document.createElement('a')
card["classList"] = "breach-card three-up ab drop-shadow send-ga-ping";
card["href"] = `/breach-details/${breach.Name}`;
card.dataset.eventCategory = "All Breaches: More about this breach";
card.dataset.eventAction = "Click";
card.dataset.eventLabel = breach.Title;
fragment.appendChild(card);
card.classList = 'breach-card three-up ab drop-shadow send-ga-ping'
card.href = `/breach-details/${breach.Name}`
card.dataset.eventCategory = 'All Breaches: More about this breach'
card.dataset.eventAction = 'Click'
card.dataset.eventLabel = breach.Title
fragment.appendChild(card)
const logoWrapper = makeDiv("breach-logo-wrapper", card);
const logoWrapper = makeDiv('breach-logo-wrapper', card)
const breachLogo = document.createElement("img");
breachLogo["alt"] = "";
breachLogo["classList"] = "breach-logo lazy-img";
breachLogo.dataset.src = `${logosOrigin}/img/logos/${breach.LogoPath}`;
breachLogo.src = "/img/logos/lazyPlaceHolder.png";
logoWrapper.appendChild(breachLogo);
const breachLogo = document.createElement('img')
breachLogo.alt = ''
breachLogo.classList = 'breach-logo lazy-img'
breachLogo.dataset.src = `${logosOrigin}/img/logos/${breach.LogoPath}`
breachLogo.src = '/img/logos/lazyPlaceHolder.png'
logoWrapper.appendChild(breachLogo)
// make wrapper for the breach-info and link
const breachInfoWrapper = makeDiv("breach-info-wrapper flx flx-col", card);
const breachInfoWrapper = makeDiv('breach-info-wrapper flx flx-col', card)
// make wrapper for the Added Date, Compromised Accounts etc info...
let wrapper = makeDiv("flx flx-col", breachInfoWrapper);
makeBreachInfoSpans("breach-title", breach.Title, wrapper);
// make wrapper for the Added Date, Compromised Accounts etc info...
let wrapper = makeDiv('flx flx-col', breachInfoWrapper)
makeBreachInfoSpans('breach-title', breach.Title, wrapper)
// added date
makeBreachInfoSpans("breach-key", LocalizedBreachCardStrings.BreachAdded, wrapper);
makeBreachInfoSpans("breach-value", breach.AddedDate, wrapper);
makeBreachInfoSpans('breach-key', LocalizedBreachCardStrings.BreachAdded, wrapper)
makeBreachInfoSpans('breach-value', breach.AddedDate, wrapper)
// compromised data
makeBreachInfoSpans("breach-key", LocalizedBreachCardStrings.CompromisedData, wrapper);
makeBreachInfoSpans("breach-value", breach.DataClasses, wrapper);
makeBreachInfoSpans('breach-key', LocalizedBreachCardStrings.CompromisedData, wrapper)
makeBreachInfoSpans('breach-value', breach.DataClasses, wrapper)
// add link at bottom of card
wrapper = makeDiv("breach-card-link-wrap", breachInfoWrapper);
makeBreachInfoSpans("blue-link more-about-this-breach", LocalizedBreachCardStrings.MoreInfoLink, wrapper);
wrapper = makeDiv('breach-card-link-wrap', breachInfoWrapper)
makeBreachInfoSpans('blue-link more-about-this-breach', LocalizedBreachCardStrings.MoreInfoLink, wrapper)
}
breachCardWrapper.appendChild(fragment);
breachCardWrapper.classList.toggle("hide-breaches");
breachLogos.lazyLoad();
const loader = document.getElementById("breaches-loader");
breachCardWrapper.appendChild(fragment)
breachCardWrapper.classList.toggle('hide-breaches')
breachLogos.lazyLoad()
const loader = document.getElementById('breaches-loader')
loader.classList = ["hide"];
return breaches;
loader.classList = ['hide']
return breaches
}
function initBreaches() {
const breachCardWrapper = document.querySelector("#all-breaches");
function initBreaches () {
const breachCardWrapper = document.querySelector('#all-breaches')
if (breachCardWrapper) {
const breachWrapper = document.getElementById("breach-array-json");
const {LocalizedBreachCardStrings, breaches} = JSON.parse(breachWrapper.dataset.breachArray);
const breachWrapper = document.getElementById('breach-array-json')
const { LocalizedBreachCardStrings, breaches } = JSON.parse(breachWrapper.dataset.breachArray)
const breachLogos = new breachImages();
document.addEventListener("scroll", breachLogos.lazyLoad);
window.addEventListener("resize", breachLogos.lazyLoad);
window.addEventListener("orientationchange", breachLogos.lazyLoad);
const breachLogos = new breachImages()
document.addEventListener('scroll', breachLogos.lazyLoad)
window.addEventListener('resize', breachLogos.lazyLoad)
window.addEventListener('orientationchange', breachLogos.lazyLoad)
const doBreaches = (arr) => {
makeBreaches(arr, LocalizedBreachCardStrings, breachCardWrapper, breachLogos);
};
makeBreaches(arr, LocalizedBreachCardStrings, breachCardWrapper, breachLogos)
}
const firstFifteen = breaches.slice(0,15);
doBreaches(firstFifteen);
const firstFifteen = breaches.slice(0, 15)
doBreaches(firstFifteen)
const noResultsBlurb = document.getElementById("no-results-blurb");
const noResultsBlurb = document.getElementById('no-results-blurb')
const fuzzyFindInput = document.getElementById("fuzzy-find-input");
const fuzzyFinder = document.getElementById("fuzzy-form");
const fuzzyFindInput = document.getElementById('fuzzy-find-input')
const fuzzyFinder = document.getElementById('fuzzy-form')
const [fuzzyShowAll, showHiddenBreaches] = document.querySelectorAll(".show-all-breaches");
const [fuzzyShowAll, showHiddenBreaches] = document.querySelectorAll('.show-all-breaches')
showHiddenBreaches.addEventListener("click", (e) => {
sendPing(e.target, "Click", "All Breaches Page");
doBreaches(breaches);
showHiddenBreaches.classList.add("hide");
});
showHiddenBreaches.addEventListener('click', (e) => {
sendPing(e.target, 'Click', 'All Breaches Page')
doBreaches(breaches)
showHiddenBreaches.classList.add('hide')
})
fuzzyShowAll.addEventListener("click", (e) => {
e.preventDefault();
fuzzyFindInput.value = "";
doBreaches(breaches);
noResultsBlurb.classList = [""];
fuzzyShowAll.classList = ["fuzzy-find-show-breaches"];
});
fuzzyShowAll.addEventListener('click', (e) => {
e.preventDefault()
fuzzyFindInput.value = ''
doBreaches(breaches)
noResultsBlurb.classList = ['']
fuzzyShowAll.classList = ['fuzzy-find-show-breaches']
})
const searchBreaches = (e) => {
e.preventDefault();
e.stopImmediatePropagation();
e.preventDefault()
e.stopImmediatePropagation()
// hide purple "Show All" button
// show button to clear fuzzy input
showHiddenBreaches.classList = ["hide"];
fuzzyShowAll.classList = ["fuzzy-find-show-breaches show"];
showHiddenBreaches.classList = ['hide']
fuzzyShowAll.classList = ['fuzzy-find-show-breaches show']
const breachSearchTerm = fuzzyFindInput.value.toLowerCase();
const breachSearchTerm = fuzzyFindInput.value.toLowerCase()
// filter breach array by search term
const filteredBreachArray = breaches.filter(breach => {
return breach.Title.toLowerCase().startsWith(breachSearchTerm);
});
return breach.Title.toLowerCase().startsWith(breachSearchTerm)
})
// if hitting enter off a zero results search, restore breaches
// and clear out input
if (e.keyCode === 13 && noResultsBlurb.classList.contains("show")) {
doBreaches(breaches);
noResultsBlurb.classList.remove("show");
fuzzyShowAll.classList.remove("show");
fuzzyFindInput.value = "";
return false;
if (e.keyCode === 13 && noResultsBlurb.classList.contains('show')) {
doBreaches(breaches)
noResultsBlurb.classList.remove('show')
fuzzyShowAll.classList.remove('show')
fuzzyFindInput.value = ''
return false
}
// if no results, show "no results message"
// otherwise make sure it isn't showing
if (filteredBreachArray.length === 0) {
noResultsBlurb.classList.add("show");
noResultsBlurb.classList.add('show')
} else {
noResultsBlurb.classList = [""];
noResultsBlurb.classList = ['']
}
doBreaches(filteredBreachArray);
return false;
};
fuzzyFinder.addEventListener("keydown", () => {
const finderInput = fuzzyFinder.querySelector("input[type=text]");
if (finderInput.value === "") {
sendPing(fuzzyFinder, "Engage", "All Breaches Page");
doBreaches(filteredBreachArray)
return false
}
fuzzyFinder.addEventListener('keydown', () => {
const finderInput = fuzzyFinder.querySelector('input[type=text]')
if (finderInput.value === '') {
sendPing(fuzzyFinder, 'Engage', 'All Breaches Page')
}
});
fuzzyFinder.addEventListener("keyup", searchBreaches);
fuzzyFinder.addEventListener("submit", searchBreaches);
})
fuzzyFinder.addEventListener('keyup', searchBreaches)
fuzzyFinder.addEventListener('submit', searchBreaches)
}
}
document.onreadystatechange = () => {
if (document.readyState === "interactive") {
initBreaches();
if (document.readyState === 'interactive') {
initBreaches()
}
};
}

Просмотреть файл

@ -1,7 +1,7 @@
"use strict";
'use strict'
/*eslint-disable no-var */
/*eslint-disable no-unused-vars */
/* eslint-disable no-var */
/* eslint-disable no-unused-vars */
/**
* Returns true or false based on whether doNotTack is enabled. It also takes into account the
@ -13,41 +13,40 @@
* @returns {boolean} true if enabled else false
*/
function _dntEnabled(dnt, userAgent) {
function _dntEnabled (dnt, userAgent) {
// for old version of IE we need to use the msDoNotTrack property of navigator
// on newer versions, and newer platforms, this is doNotTrack but, on the window object
// Safari also exposes the property on the window object.
var dntStatus = dnt || navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack
var ua = userAgent || navigator.userAgent
// for old version of IE we need to use the msDoNotTrack property of navigator
// on newer versions, and newer platforms, this is doNotTrack but, on the window object
// Safari also exposes the property on the window object.
var dntStatus = dnt || navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
var ua = userAgent || navigator.userAgent;
// List of Windows versions known to not implement DNT according to the standard.
var anomalousWinVersions = ['Windows NT 6.1', 'Windows NT 6.2', 'Windows NT 6.3']
// List of Windows versions known to not implement DNT according to the standard.
var anomalousWinVersions = ["Windows NT 6.1", "Windows NT 6.2", "Windows NT 6.3"];
var fxMatch = ua.match(/Firefox\/(\d+)/)
var ieRegEx = /MSIE|Trident/i
var isIE = ieRegEx.test(ua)
// Matches from Windows up to the first occurance of ; un-greedily
// http://www.regexr.com/3c2el
var platform = ua.match(/Windows.+?(?=;)/g)
var fxMatch = ua.match(/Firefox\/(\d+)/);
var ieRegEx = /MSIE|Trident/i;
var isIE = ieRegEx.test(ua);
// Matches from Windows up to the first occurance of ; un-greedily
// http://www.regexr.com/3c2el
var platform = ua.match(/Windows.+?(?=;)/g);
// With old versions of IE, DNT did not exist so we simply return false;
if (isIE && typeof Array.prototype.indexOf !== 'function') {
return false
} else if (fxMatch && parseInt(fxMatch[1], 10) < 32) {
// Can"t say for sure if it is 1 or 0, due to Fx bug 887703
dntStatus = 'Unspecified'
} else if (isIE && platform && anomalousWinVersions.indexOf(platform.toString()) !== -1) {
// default is on, which does not honor the specification
dntStatus = 'Unspecified'
} else {
// sets dntStatus to Disabled or Enabled based on the value returned by the browser.
// If dntStatus is undefined, it will be set to Unspecified
dntStatus = { 0: 'Disabled', 1: 'Enabled' }[dntStatus] || 'Unspecified'
}
// With old versions of IE, DNT did not exist so we simply return false;
if (isIE && typeof Array.prototype.indexOf !== "function") {
return false;
} else if (fxMatch && parseInt(fxMatch[1], 10) < 32) {
// Can"t say for sure if it is 1 or 0, due to Fx bug 887703
dntStatus = "Unspecified";
} else if (isIE && platform && anomalousWinVersions.indexOf(platform.toString()) !== -1) {
// default is on, which does not honor the specification
dntStatus = "Unspecified";
} else {
// sets dntStatus to Disabled or Enabled based on the value returned by the browser.
// If dntStatus is undefined, it will be set to Unspecified
dntStatus = { "0": "Disabled", "1": "Enabled" }[dntStatus] || "Unspecified";
}
return dntStatus === "Enabled" ? true : false;
return dntStatus === 'Enabled'
}
/*eslint-enable no-var */
/*eslint-enable no-unused-vars */
/* eslint-enable no-var */
/* eslint-enable no-unused-vars */

Просмотреть файл

@ -1,109 +1,106 @@
"use strict";
'use strict'
/* global findAncestor */
async function sendForm(action, formBody={}) {
async function sendForm (action, formBody = {}) {
const response = await fetch(`/user/${action}`, {
headers: {
"Content-Type": "application/json; charset=utf-8",
'Content-Type': 'application/json; charset=utf-8'
},
mode: "same-origin",
method: "POST",
body: JSON.stringify(formBody),
});
mode: 'same-origin',
method: 'POST',
body: JSON.stringify(formBody)
})
if (response.redirected) {
window.location = response.url;
return;
window.location = response.url
return
}
return await response.json();
return await response.json()
}
async function sendCommunicationOption(e) {
const { formAction, commOption, csrfToken } = e.target.dataset;
async function sendCommunicationOption (e) {
const { formAction, commOption, csrfToken } = e.target.dataset
sendForm(formAction, { communicationOption: commOption, _csrf: csrfToken })
.then(data => {}) /*decide what to do with data */
.catch(e => {})/* decide how to handle errors */;
.then(data => {}) /* decide what to do with data */
.catch(e => {})/* decide how to handle errors */
}
async function resendEmail(e) {
const resendEmailBtn = e.target;
const { formAction, csrfToken, emailId } = resendEmailBtn.dataset;
resendEmailBtn.classList.add("email-sent");
async function resendEmail (e) {
const resendEmailBtn = e.target
const { formAction, csrfToken, emailId } = resendEmailBtn.dataset
resendEmailBtn.classList.add('email-sent')
await sendForm(formAction, { _csrf: csrfToken, emailId })
.then(data => {
setTimeout( ()=> {
const span = resendEmailBtn.nextElementSibling;
span.classList.remove("hide");
}, 1000);
}) /*decide what to do with data */
.catch(e => {})/* decide how to handle errors */;
.then(data => {
setTimeout(() => {
const span = resendEmailBtn.nextElementSibling
span.classList.remove('hide')
}, 1000)
}) /* decide what to do with data */
.catch(e => {})/* decide how to handle errors */
}
function hideShowOverflowBreaches(showBreachesButton, overflowBreaches) {
function hideShowOverflowBreaches (showBreachesButton, overflowBreaches) {
[showBreachesButton, overflowBreaches].forEach(el => {
["show", "hide"].forEach(className => {
el.classList.toggle(className);
});
});
['show', 'hide'].forEach(className => {
el.classList.toggle(className)
})
})
}
function showRemainingBreaches(e) {
const showBreachesButton = e.target;
const emailCard = findAncestor(e.target, "email-card");
const additionalBreaches = emailCard.querySelector(".show-additional-breaches");
hideShowOverflowBreaches(showBreachesButton, additionalBreaches);
function showRemainingBreaches (e) {
const showBreachesButton = e.target
const emailCard = findAncestor(e.target, 'email-card')
const additionalBreaches = emailCard.querySelector('.show-additional-breaches')
hideShowOverflowBreaches(showBreachesButton, additionalBreaches)
}
if (document.querySelector(".email-card")) {
document.querySelectorAll(".show-remaining-breaches").forEach(btn => {
btn.addEventListener("click", showRemainingBreaches);
});
if (document.querySelector('.email-card')) {
document.querySelectorAll('.show-remaining-breaches').forEach(btn => {
btn.addEventListener('click', showRemainingBreaches)
})
// add listeners to "Hide / Show Resolved" buttons
document.querySelectorAll(".toggle-resolved-breaches").forEach(btn => {
btn.addEventListener("click", () => {
const emailCard = findAncestor(btn, "email-card");
emailCard.classList.toggle("show-resolved-breach-cards");
const showBreachesButton = emailCard.querySelector(".show-remaining-breaches");
if (showBreachesButton && !showBreachesButton.classList.contains("hide")) {
const additionalBreaches = emailCard.querySelector(".show-additional-breaches");
hideShowOverflowBreaches(showBreachesButton, additionalBreaches);
document.querySelectorAll('.toggle-resolved-breaches').forEach(btn => {
btn.addEventListener('click', () => {
const emailCard = findAncestor(btn, 'email-card')
emailCard.classList.toggle('show-resolved-breach-cards')
const showBreachesButton = emailCard.querySelector('.show-remaining-breaches')
if (showBreachesButton && !showBreachesButton.classList.contains('hide')) {
const additionalBreaches = emailCard.querySelector('.show-additional-breaches')
hideShowOverflowBreaches(showBreachesButton, additionalBreaches)
}
});
});
})
})
const removeEmailButtons = document.querySelectorAll(".resend-email");
const removeEmailButtons = document.querySelectorAll('.resend-email')
removeEmailButtons.forEach(btn => {
btn.addEventListener("click", resendEmail);
});
btn.addEventListener('click', resendEmail)
})
const communicationRadioButtons = document.querySelectorAll(".radio-comm-option");
const communicationRadioButtons = document.querySelectorAll('.radio-comm-option')
communicationRadioButtons.forEach(option => {
option.addEventListener("click", sendCommunicationOption);
});
option.addEventListener('click', sendCommunicationOption)
})
}
const removeMonitorButton = document.querySelector(".remove-fxm");
const removeMonitorButton = document.querySelector('.remove-fxm')
if (removeMonitorButton) {
removeMonitorButton.addEventListener("click", async (e) => {
const {formAction, csrfToken, primaryToken, primaryHash} = e.target.dataset;
await sendForm(formAction, {_csrf: csrfToken, primaryToken, primaryHash});
});
removeMonitorButton.addEventListener('click', async (e) => {
const { formAction, csrfToken, primaryToken, primaryHash } = e.target.dataset
await sendForm(formAction, { _csrf: csrfToken, primaryToken, primaryHash })
})
}
const relayLink = document.querySelector("[data-event-label='Try Firefox Relay']");
const userEmailElement = document.querySelector(".nav-user-email");
const relayLink = document.querySelector("[data-event-label='Try Firefox Relay']")
const userEmailElement = document.querySelector('.nav-user-email')
if (userEmailElement && relayLink) {
const user_email = userEmailElement.textContent;
const user_email = userEmailElement.textContent
if (user_email) {
const relayUrl = new URL(relayLink.href);
relayUrl.pathname += "accounts/fxa/login/";
relayUrl.searchParams.append("process", "login");
relayUrl.searchParams.append("auth_params", "prompt=none&login_hint=" + user_email);
relayLink.href = relayUrl.href;
const relayUrl = new URL(relayLink.href)
relayUrl.pathname += 'accounts/fxa/login/'
relayUrl.searchParams.append('process', 'login')
relayUrl.searchParams.append('auth_params', 'prompt=none&login_hint=' + user_email)
relayLink.href = relayUrl.href
}
}

Просмотреть файл

@ -1,312 +1,306 @@
"use strict";
'use strict'
/* global ga */
function getFxAppLinkInfo(localizedBentoStrings, referringSiteURL) {
function getFxAppLinkInfo (localizedBentoStrings, referringSiteURL) {
return [
[localizedBentoStrings.fxMonitor, "https://monitor.firefox.com/", "fx-monitor"],
[localizedBentoStrings.pocket, "https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447", "pocket"],
[localizedBentoStrings.fxDesktop, `https://www.mozilla.org/firefox/new/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "fx-desktop"],
[localizedBentoStrings.fxMobile, `http://mozilla.org/firefox/mobile?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "fx-mobile"],
[localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "moz-vpn"],
];
[localizedBentoStrings.fxMonitor, 'https://monitor.firefox.com/', 'fx-monitor'],
[localizedBentoStrings.pocket, 'https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447', 'pocket'],
[localizedBentoStrings.fxDesktop, `https://www.mozilla.org/firefox/new/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'fx-desktop'],
[localizedBentoStrings.fxMobile, `http://mozilla.org/firefox/mobile?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'fx-mobile'],
[localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'moz-vpn']
]
}
function createAndAppendEl(wrapper, tagName, className = null) {
const newEl = document.createElement(tagName);
function createAndAppendEl (wrapper, tagName, className = null) {
const newEl = document.createElement(tagName)
if (className) {
newEl.setAttribute("class", className);
newEl.setAttribute('class', className)
}
wrapper.appendChild(newEl);
return newEl;
wrapper.appendChild(newEl)
return newEl
}
async function getlocalizedBentoStrings() {
let localizedBentoStrings;
async function getlocalizedBentoStrings () {
let localizedBentoStrings
try {
const serverUrl = document.body.dataset.serverUrl;
const serverUrl = document.body.dataset.serverUrl
const res = await fetch(
`${serverUrl}/getBentoStrings`,
{
mode: "cors",
mode: 'cors'
}
);
localizedBentoStrings = await res.json();
} catch(e) {
)
localizedBentoStrings = await res.json()
} catch (e) {
// Error fetching the localized strings. Defaulting to English.
localizedBentoStrings = {
bentoButtonTitle: "Firefox apps and services",
bentoHeadline: "Firefox is tech that fights for your online privacy.",
bentoBottomLink: "Made by Mozilla",
mobileCloseBentoButtonTitle: "Close menu",
fxDesktop: "Firefox Browser for Desktop",
fxMobile: "Firefox Browser for Mobile",
fxMonitor: "Firefox Monitor",
pocket: "Pocket",
mozVPN: "Mozilla VPN",
};
bentoButtonTitle: 'Firefox apps and services',
bentoHeadline: 'Firefox is tech that fights for your online privacy.',
bentoBottomLink: 'Made by Mozilla',
mobileCloseBentoButtonTitle: 'Close menu',
fxDesktop: 'Firefox Browser for Desktop',
fxMobile: 'Firefox Browser for Mobile',
fxMonitor: 'Firefox Monitor',
pocket: 'Pocket',
mozVPN: 'Mozilla VPN'
}
}
return localizedBentoStrings;
return localizedBentoStrings
}
class FirefoxApps extends HTMLElement {
constructor() {
super();
constructor () {
super()
}
async connectedCallback() {
this._currentSite = document.body.dataset.bentoAppId;
this._localizedBentoStrings = await getlocalizedBentoStrings();
async connectedCallback () {
this._currentSite = document.body.dataset.bentoAppId
this._localizedBentoStrings = await getlocalizedBentoStrings()
this._active = false; // Becomes true when the bento is opened.
this._active = false // Becomes true when the bento is opened.
this._frag = document.createDocumentFragment(); // Wrapping fragment for bento button and bento content.
this._frag = document.createDocumentFragment() // Wrapping fragment for bento button and bento content.
this._bentoButton = createAndAppendEl(this._frag, "button", "fx-bento-button toggle-bento"); // Button toggles dropdown.
this.addTitleAndAriaLabel(this._bentoButton, this._localizedBentoStrings.bentoButtonTitle);
this._bentoButton = createAndAppendEl(this._frag, 'button', 'fx-bento-button toggle-bento') // Button toggles dropdown.
this.addTitleAndAriaLabel(this._bentoButton, this._localizedBentoStrings.bentoButtonTitle)
this._bentoWrapper = document.createElement("div");
this._bentoWrapper.classList = "fx-bento-content-wrapper";
this._bentoWrapper = document.createElement('div')
this._bentoWrapper.classList = 'fx-bento-content-wrapper'
const browserLanguage = window.navigator.language;
if (!browserLanguage.includes("en")) {
this._bentoWrapper.classList.add("fx-bento-hide-vpn");
const browserLanguage = window.navigator.language
if (!browserLanguage.includes('en')) {
this._bentoWrapper.classList.add('fx-bento-hide-vpn')
}
this._bentoHideOverflow = createAndAppendEl(this._bentoWrapper, 'div', 'fx-bento-hide-overflow')
this._bentoContent = createAndAppendEl(this._bentoHideOverflow, 'div', 'fx-bento-content')
this._bentoHideOverflow = createAndAppendEl(this._bentoWrapper, "div", "fx-bento-hide-overflow");
this._bentoContent = createAndAppendEl(this._bentoHideOverflow, "div", "fx-bento-content");
this._mobileCloseBentoButton = createAndAppendEl(this._bentoContent, "button", "fx-bento-mobile-close toggle-bento");
this._mobileCloseBentoButton = createAndAppendEl(this._bentoContent, 'button', 'fx-bento-mobile-close toggle-bento')
this.addTitleAndAriaLabel(this._mobileCloseBentoButton, this._localizedBentoStrings.mobileCloseBentoButtonTitle);
[this._bentoButton, this._mobileCloseBentoButton].forEach(btn => {
btn.addEventListener("click", this);
});
btn.addEventListener('click', this)
})
this._logoHeadlineWrapper = createAndAppendEl(this._bentoContent, "div", "fx-bento-headline-logo-wrapper");
this._firefoxLogo = createAndAppendEl( this._logoHeadlineWrapper, "div", "fx-bento-logo");
this._messageTop = createAndAppendEl( this._logoHeadlineWrapper, "span", "fx-bento-headline");
this._messageTop.textContent = this._localizedBentoStrings.bentoHeadline;
this._logoHeadlineWrapper = createAndAppendEl(this._bentoContent, 'div', 'fx-bento-headline-logo-wrapper')
this._firefoxLogo = createAndAppendEl(this._logoHeadlineWrapper, 'div', 'fx-bento-logo')
this._messageTop = createAndAppendEl(this._logoHeadlineWrapper, 'span', 'fx-bento-headline')
this._messageTop.textContent = this._localizedBentoStrings.bentoHeadline
this._appList = this.makeAppList();
this._appList = this.makeAppList()
this._messageBottomLink = createAndAppendEl(this._bentoContent, "a", "fx-bento-bottom-link fx-bento-link");
this._messageBottomLink.textContent = this._localizedBentoStrings.bentoBottomLink;
this._messageBottomLink.href = "https://www.mozilla.org/";
this._messageBottomLink = createAndAppendEl(this._bentoContent, 'a', 'fx-bento-bottom-link fx-bento-link')
this._messageBottomLink.textContent = this._localizedBentoStrings.bentoBottomLink
this._messageBottomLink.href = 'https://www.mozilla.org/'
this._bentoContent.querySelectorAll("a").forEach( (anchorEl, idx) => {
anchorEl.dataset.bentoLinkOrder = idx;
anchorEl.addEventListener("click", this);
anchorEl.tabIndex = "-1";
});
this._bentoContent.querySelectorAll('a').forEach((anchorEl, idx) => {
anchorEl.dataset.bentoLinkOrder = idx
anchorEl.addEventListener('click', this)
anchorEl.tabIndex = '-1'
})
this._frag.appendChild(this._bentoWrapper);
this.appendChild(this._frag);
this.addEventListener("close-bento-menu", this);
this._frag.appendChild(this._bentoWrapper)
this.appendChild(this._frag)
this.addEventListener('close-bento-menu', this)
}
addTitleAndAriaLabel(el, localizedCopy) {
["title", "aria-label"].forEach(attrName => {
el.setAttribute(attrName, localizedCopy);
});
addTitleAndAriaLabel (el, localizedCopy) {
['title', 'aria-label'].forEach(attrName => {
el.setAttribute(attrName, localizedCopy)
})
}
metricsSendEvent(eventAction, eventLabel) {
if (typeof(ga) !== "undefined") {
return ga("send", "event", "bento", eventAction, eventLabel);
metricsSendEvent (eventAction, eventLabel) {
if (typeof (ga) !== 'undefined') {
return ga('send', 'event', 'bento', eventAction, eventLabel)
}
}
handleEvent(event) {
handleEvent (event) {
const closeBento = () => {
this.handleBentoFocusTrap();
window.removeEventListener("resize", this.handleBentoHeight);
window.removeEventListener("click", this);
document.removeEventListener("keydown", this);
this.metricsSendEvent("bento-closed", this._currentSite);
this.classList.remove("fx-bento-open");
this._bentoWrapper.classList.add("fx-bento-fade-out");
this.handleBentoFocusTrap()
window.removeEventListener('resize', this.handleBentoHeight)
window.removeEventListener('click', this)
document.removeEventListener('keydown', this)
this.metricsSendEvent('bento-closed', this._currentSite)
this.classList.remove('fx-bento-open')
this._bentoWrapper.classList.add('fx-bento-fade-out')
setTimeout(() => {
this._bentoWrapper.classList.remove("fx-bento-fade-out");
this._bentoButton.blur();
this.classList = [];
}, 500);
return;
};
this._bentoWrapper.classList.remove('fx-bento-fade-out')
this._bentoButton.blur()
this.classList = []
}, 500)
}
const keydownEvent = (event.type === "keydown");
const eventTarget = event.target;
const keydownEvent = (event.type === 'keydown')
const eventTarget = event.target
if (
// ignore mouse clicks inside the bento
(!keydownEvent && ["fx-bento-content active", "fx-bento-headline", "fx-bento-logo", "fx-bento-headline-logo-wrapper"].includes(eventTarget.className)) ||
(!keydownEvent && ['fx-bento-content active', 'fx-bento-headline', 'fx-bento-logo', 'fx-bento-headline-logo-wrapper'].includes(eventTarget.className)) ||
// ignore and don't prevent default behavior on key clicks other than Escape, Down Arrow, and Up Arrow
(keydownEvent && ![27, 40, 38].includes(event.keyCode))
) {
return;
) {
return
}
const hasParent = (el, selector) => {
while (el.parentElement) {
el = el.parentElement;
if (el.tagName === selector)
return el;
el = el.parentElement
if (el.tagName === selector) { return el }
}
return null;
};
// close Bento on mouse clicks outside the Bento menu
if (hasParent(event.target, "FIREFOX-APPS") === null) {
this._active = !this._active;
return closeBento();
return null
}
event.preventDefault();
event.stopPropagation();
// close Bento on mouse clicks outside the Bento menu
if (hasParent(event.target, 'FIREFOX-APPS') === null) {
this._active = !this._active
return closeBento()
}
event.preventDefault()
event.stopPropagation()
if (keydownEvent) {
const moveFocusWithArrows = (whichDirection) => {
const activeEl = document.activeElement;
const bentoLinks = this._bentoContent.querySelectorAll("a");
const activeEl = document.activeElement
const bentoLinks = this._bentoContent.querySelectorAll('a')
if (!activeEl.dataset.bentoLinkOrder) { // check if link in Bento has focus
bentoLinks[0].focus(); // focus first link in bento
return;
bentoLinks[0].focus() // focus first link in bento
return
}
const activeLinkNum = parseInt(activeEl.dataset.bentoLinkOrder);
const newActiveLink = parseInt(activeLinkNum + whichDirection);
const activeLinkNum = parseInt(activeEl.dataset.bentoLinkOrder)
const newActiveLink = parseInt(activeLinkNum + whichDirection)
if (bentoLinks[newActiveLink]) {
bentoLinks[newActiveLink].focus();
bentoLinks[newActiveLink].focus()
}
return;
};
switch(event.keyCode) {
case 27: // escape
this._active = !this._active;
closeBento();
return;
case 40 : // down arrow || up arrow
moveFocusWithArrows(1);
break;
case 38: // arrow up
moveFocusWithArrows(-1);
break;
}
return;
switch (event.keyCode) {
case 27: // escape
this._active = !this._active
closeBento()
return
case 40 : // down arrow || up arrow
moveFocusWithArrows(1)
break
case 38: // arrow up
moveFocusWithArrows(-1)
break
}
return
}
this._active = !this._active;
const eventTargetClassList = event.target.classList;
const MozLinkClick = (eventTargetClassList.contains("fx-bento-bottom-link"));
this._active = !this._active
const eventTargetClassList = event.target.classList
const MozLinkClick = (eventTargetClassList.contains('fx-bento-bottom-link'))
if (eventTargetClassList.contains("fx-bento-app-link") || MozLinkClick) {
const url = new URL(eventTarget.href); // add any additional UTM params - or whatever
url.searchParams.append("utm_source", this._currentSite);
url.searchParams.append("utm_medium", "referral");
url.searchParams.append("utm_campaign", "bento");
if (eventTargetClassList.contains('fx-bento-app-link') || MozLinkClick) {
const url = new URL(eventTarget.href) // add any additional UTM params - or whatever
url.searchParams.append('utm_source', this._currentSite)
url.searchParams.append('utm_medium', 'referral')
url.searchParams.append('utm_campaign', 'bento')
if (MozLinkClick) {
this.metricsSendEvent("bento-app-link-click", "Mozilla");
window.open(url, "_blank", "noopener");
return closeBento();
this.metricsSendEvent('bento-app-link-click', 'Mozilla')
window.open(url, '_blank', 'noopener')
return closeBento()
}
const appToOpenId = eventTarget.dataset.bentoAppLinkId;
this.metricsSendEvent("bento-app-link-click", appToOpenId);
if (eventTargetClassList.contains("fx-bento-current-site")) { // open index page in existing window
window.location = url;
return closeBento();
const appToOpenId = eventTarget.dataset.bentoAppLinkId
this.metricsSendEvent('bento-app-link-click', appToOpenId)
if (eventTargetClassList.contains('fx-bento-current-site')) { // open index page in existing window
window.location = url
return closeBento()
}
window.open(url, "_blank", "noopener");
return closeBento();
window.open(url, '_blank', 'noopener')
return closeBento()
}
if (!this._active) {
return closeBento();
return closeBento()
}
const sendEventOnBentoOpen = new Event("bento-was-opened");
document.dispatchEvent(sendEventOnBentoOpen);
const sendEventOnBentoOpen = new Event('bento-was-opened')
document.dispatchEvent(sendEventOnBentoOpen)
this.metricsSendEvent("bento-opened", this._currentSite);
this.handleBentoHeight();
document.addEventListener("keydown", this);
window.addEventListener("resize", this.handleBentoHeight);
window.addEventListener("click", this);
this.metricsSendEvent('bento-opened', this._currentSite)
this.handleBentoHeight()
document.addEventListener('keydown', this)
window.addEventListener('resize', this.handleBentoHeight)
window.addEventListener('click', this)
this.classList = ["active fx-bento-open"];
this._bentoButton.focus();
return this.handleBentoFocusTrap();
this.classList = ['active fx-bento-open']
this._bentoButton.focus()
return this.handleBentoFocusTrap()
}
handleBentoHeight() { // resize bento max-height if necessary
const bento = document.querySelector(".fx-bento-content");
const winHeight = window.innerHeight;
const newBentoHeight = winHeight - bento.offsetTop - 100;
const setMaxHeight = (winHeight < 500 && window.innerWidth > 500);
handleBentoHeight () { // resize bento max-height if necessary
const bento = document.querySelector('.fx-bento-content')
const winHeight = window.innerHeight
const newBentoHeight = winHeight - bento.offsetTop - 100
const setMaxHeight = (winHeight < 500 && window.innerWidth > 500)
if (setMaxHeight) {
bento.style.maxHeight = `${newBentoHeight}px`;
bento.style.maxHeight = `${newBentoHeight}px`
} else {
bento.style.maxHeight = "1000px";
bento.style.maxHeight = '1000px'
}
bento.classList.toggle("fx-bento-enable-scrolling", setMaxHeight);
}
bento.classList.toggle('fx-bento-enable-scrolling', setMaxHeight)
}
handleBentoFocusTrap() {
handleBentoFocusTrap () {
const nonBentoPageElements = document.querySelectorAll(
"a:not(.fx-bento-app-link):not(.fx-bento-bottom-link), button:not(.toggle-bento ), input, select, option, [tabindex]"
);
const bentoLinks = this._bentoContent.querySelectorAll(".fx-bento-app-link, .fx-bento-bottom-link");
'a:not(.fx-bento-app-link):not(.fx-bento-bottom-link), button:not(.toggle-bento ), input, select, option, [tabindex]'
)
const bentoLinks = this._bentoContent.querySelectorAll('.fx-bento-app-link, .fx-bento-bottom-link')
if (this._active) {
nonBentoPageElements.forEach(el => {
if (el.tabIndex > -1) {
el.dataset.oldTabIndex = el.tabIndex;
el.dataset.oldTabIndex = el.tabIndex
}
el.tabIndex = -1;
});
el.tabIndex = -1
})
bentoLinks.forEach(el => {
el.tabIndex = 0;
});
return;
el.tabIndex = 0
})
return
}
nonBentoPageElements.forEach(el => {
if (el.dataset.oldTabIndex) {
el.tabIndex = el.dataset.oldTabIndex;
delete el.dataset.oldTabIndex;
return;
el.tabIndex = el.dataset.oldTabIndex
delete el.dataset.oldTabIndex
return
}
el.tabIndex = 0;
});
el.tabIndex = 0
})
bentoLinks.forEach(el => {
el.tabIndex = -1;
});
el.tabIndex = -1
})
}
makeAppList() {
makeAppList () {
// const browserLanguage = window.navigator.language
const appLinks = getFxAppLinkInfo(this._localizedBentoStrings, this._currentSite);
const appLinks = getFxAppLinkInfo(this._localizedBentoStrings, this._currentSite)
// appLinks.push([localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "moz-vpn"])
appLinks.forEach(app => {
const newLink = document.createElement("a");
const newLinkSpan = createAndAppendEl(newLink, "span", `fx-bento-app-link-span ${app[2]}`);
newLink.setAttribute("class", `fx-bento-app-link fx-bento-link ${app[2]}`);
newLinkSpan["textContent"] = app[0];
["href", "data-bento-app-link-id"].forEach((attributeName, index) => {
newLink.setAttribute(attributeName, app[index + 1]);
});
const newLink = document.createElement('a')
const newLinkSpan = createAndAppendEl(newLink, 'span', `fx-bento-app-link-span ${app[2]}`)
newLink.setAttribute('class', `fx-bento-app-link fx-bento-link ${app[2]}`)
newLinkSpan.textContent = app[0];
['href', 'data-bento-app-link-id'].forEach((attributeName, index) => {
newLink.setAttribute(attributeName, app[index + 1])
})
if (newLink.dataset.bentoAppLinkId === this._currentSite) {
newLink.classList.add("fx-bento-current-site");
newLink.classList.add('fx-bento-current-site')
}
this._bentoContent.appendChild(newLink);
});
this._bentoContent.appendChild(newLink)
})
}
}
if (typeof(customElements) !== "undefined") {
customElements.define("firefox-apps", FirefoxApps);
if (typeof (customElements) !== 'undefined') {
customElements.define('firefox-apps', FirefoxApps)
} else { // Hide on unsupportive browsers
document.addEventListener("DOMContentLoaded", () => {
document.body.classList.add("hide-bento");
});
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('hide-bento')
})
}

Просмотреть файл

@ -1,239 +1,231 @@
"use strict";
'use strict'
/* eslint-disable no-unused-vars */
/* global _dntEnabled */
/* global ga */
const hasParent = (el, selector) => {
while (el.parentNode) {
el = el.parentNode;
if (el.dataset && el.dataset.analyticsId === selector)
return el;
el = el.parentNode
if (el.dataset && el.dataset.analyticsId === selector) { return el }
}
return null;
};
return null
}
function getLocation() {
const eventLocation = document.querySelectorAll("[data-page-label]");
function getLocation () {
const eventLocation = document.querySelectorAll('[data-page-label]')
if (eventLocation.length > 0) {
return `Page ID: ${eventLocation[0].dataset.pageLabel}`;
return `Page ID: ${eventLocation[0].dataset.pageLabel}`
} else {
return "Page ID: Undefined Page";
return 'Page ID: Undefined Page'
}
}
async function sendPing(el, eventAction, eventLabel = null, options = null) {
if (typeof(ga) !== "undefined" && !el.classList.contains("hide")) {
async function sendPing (el, eventAction, eventLabel = null, options = null) {
if (typeof (ga) !== 'undefined' && !el.classList.contains('hide')) {
if (!eventLabel) {
eventLabel = `${getLocation()}`;
eventLabel = `${getLocation()}`
}
const eventCategory = `[v2] ${el.dataset.eventCategory}`;
return ga("send", "event", eventCategory, eventAction, eventLabel, options);
const eventCategory = `[v2] ${el.dataset.eventCategory}`
return ga('send', 'event', eventCategory, eventAction, eventLabel, options)
}
}
function appendFxaParams(url, storageObject) {
function appendFxaParams (url, storageObject) {
getUTMNames().forEach(param => {
if (storageObject[param] && !url.searchParams.get(param)) {
// Bug #2011 - This logic only allows params to be set/passed
// on to FxA if that param isn't already set.
// (Example: Overwriting a utm_source)
url.searchParams.append(param, encodeURIComponent(storageObject[param]));
url.searchParams.append(param, encodeURIComponent(storageObject[param]))
}
});
return url;
})
return url
}
function getFxaUtms(url) {
function getFxaUtms (url) {
if (sessionStorage) {
url = appendFxaParams(url, sessionStorage);
url = appendFxaParams(url, sessionStorage)
}
return appendFxaParams(url, document.body.dataset);
return appendFxaParams(url, document.body.dataset)
}
function saveReferringPageData(utmParams) {
function saveReferringPageData (utmParams) {
if (sessionStorage) {
getUTMNames().forEach(param => {
if(utmParams.get(param)) {
sessionStorage[param] = utmParams.get(param);
if (utmParams.get(param)) {
sessionStorage[param] = utmParams.get(param)
}
});
return;
})
}
}
function getUTMNames() {
return ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
function getUTMNames () {
return ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
}
function sendRecommendationPings(ctaSelector) {
function sendRecommendationPings (ctaSelector) {
document.querySelectorAll(ctaSelector).forEach(cta => {
const eventLabel = cta.dataset.eventLabel;
ga("send", "event", "Breach Detail: Recommendation CTA", "View", eventLabel, {nonInteraction: true});
cta.addEventListener("click", () => {
ga("send", "event", "Breach Detail: Recommendation CTA", "Engage", eventLabel, {transport: "beacon"});
});
});
const eventLabel = cta.dataset.eventLabel
ga('send', 'event', 'Breach Detail: Recommendation CTA', 'View', eventLabel, { nonInteraction: true })
cta.addEventListener('click', () => {
ga('send', 'event', 'Breach Detail: Recommendation CTA', 'Engage', eventLabel, { transport: 'beacon' })
})
})
}
function setMetricsIds(el) {
if (el.dataset.entrypoint && hasParent(el, "sign-up-banner")) {
el.dataset.eventCategory = `${el.dataset.eventCategory} - Banner`;
el.dataset.entrypoint = `${el.dataset.entrypoint}-banner`;
function setMetricsIds (el) {
if (el.dataset.entrypoint && hasParent(el, 'sign-up-banner')) {
el.dataset.eventCategory = `${el.dataset.eventCategory} - Banner`
el.dataset.entrypoint = `${el.dataset.entrypoint}-banner`
}
return;
}
function setGAListeners(){
function setGAListeners () {
// Send "View" pings for any visible recommendation CTAs.
sendRecommendationPings(".first-four-recs");
sendRecommendationPings('.first-four-recs')
document.querySelectorAll(".send-ga-ping, [data-send-ga-ping]").forEach((el) => {
el.addEventListener("click", (e) => {
const eventCategory = e.target.dataset.eventCategory;
const eventAction = e.target.dataset.eventAction;
const eventLabel = e.target.dataset.eventLabel;
ga("send", "event", eventCategory, eventAction, eventLabel, {transport: "beacon"});
});
});
document.querySelectorAll('.send-ga-ping, [data-send-ga-ping]').forEach((el) => {
el.addEventListener('click', (e) => {
const eventCategory = e.target.dataset.eventCategory
const eventAction = e.target.dataset.eventAction
const eventLabel = e.target.dataset.eventLabel
ga('send', 'event', eventCategory, eventAction, eventLabel, { transport: 'beacon' })
})
})
// Update data-event-category and data-fxa-entrypoint if the element
// is nested inside a sign up banner.
document.querySelectorAll("#scan-user-email, .open-oauth").forEach(el => {
setMetricsIds(el);
});
document.querySelectorAll('#scan-user-email, .open-oauth').forEach(el => {
setMetricsIds(el)
})
document.querySelectorAll(".open-oauth").forEach( async(el) => {
const fxaUrl = new URL("/metrics-flow?", document.body.dataset.fxaAddress);
document.querySelectorAll('.open-oauth').forEach(async (el) => {
const fxaUrl = new URL('/metrics-flow?', document.body.dataset.fxaAddress)
try {
const response = await fetch(fxaUrl, {credentials: "omit"});
fxaUrl.searchParams.append("entrypoint", encodeURIComponent(el.dataset.entrypoint));
const response = await fetch(fxaUrl, { credentials: 'omit' })
fxaUrl.searchParams.append('entrypoint', encodeURIComponent(el.dataset.entrypoint))
if (response && response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
el.dataset.flowId = flowId;
el.dataset.flowBeginTime = flowBeginTime;
const { flowId, flowBeginTime } = await response.json()
el.dataset.flowId = flowId
el.dataset.flowBeginTime = flowBeginTime
}
} catch(e) {
} catch (e) {
// should we do anything with this?
}
});
})
if (typeof(ga) !== "undefined") {
const pageLocation = getLocation();
if (typeof (ga) !== 'undefined') {
const pageLocation = getLocation()
// Elements for which we send Google Analytics "View" pings...
const eventTriggers = [
"#scan-user-email",
"#add-another-email-form",
".open-oauth:not(.product-promo-wrapper)", // The promo entrypoint events are handled elsewhere.
"#vpnPromoCloseButton",
];
'#scan-user-email',
'#add-another-email-form',
'.open-oauth:not(.product-promo-wrapper)', // The promo entrypoint events are handled elsewhere.
'#vpnPromoCloseButton'
]
// Send number of foundBreaches on Scan, Full Report, and User Dashboard pageviews
if (pageLocation === ("Scan Results")) {
const breaches = document.querySelectorAll(".breach-card");
ga("send", "event", "[v2] Breach Count", "Returned Breaches", `${pageLocation}`, breaches.length);
if (pageLocation === ('Scan Results')) {
const breaches = document.querySelectorAll('.breach-card')
ga('send', 'event', '[v2] Breach Count', 'Returned Breaches', `${pageLocation}`, breaches.length)
}
// Send "View" pings and add event listeners.
document.querySelectorAll(eventTriggers).forEach(el => {
sendPing(el, "View", pageLocation, {nonInteraction: true});
if (["BUTTON", "A"].includes(el.tagName)) {
el.addEventListener("click", async(e) => {
await sendPing(el, "Engage", pageLocation, {transport: "beacon"});
});
sendPing(el, 'View', pageLocation, { nonInteraction: true })
if (['BUTTON', 'A'].includes(el.tagName)) {
el.addEventListener('click', async (e) => {
await sendPing(el, 'Engage', pageLocation, { transport: 'beacon' })
})
}
});
})
// Add event listeners to event triggering elements
// for which we do not send "View" pings.
document.querySelectorAll("[data-ga-link]").forEach((el) => {
el.addEventListener("click", async(e) => {
const linkId = `Link ID: ${e.target.dataset.eventLabel}`;
await sendPing(el, "Click", `${linkId}`);
});
});
document.querySelectorAll('[data-ga-link]').forEach((el) => {
el.addEventListener('click', async (e) => {
const linkId = `Link ID: ${e.target.dataset.eventLabel}`
await sendPing(el, 'Click', `${linkId}`)
})
})
document.querySelectorAll("video").forEach((el) => {
el.addEventListener("play", async (e) => {
if (e.target.currentTime > 0) return; // only track initial play event
e.target.dataset.eventCategory = "video play";
await sendPing(e.target, "Click", e.target.src);
});
});
document.querySelectorAll('video').forEach((el) => {
el.addEventListener('play', async (e) => {
if (e.target.currentTime > 0) return // only track initial play event
e.target.dataset.eventCategory = 'video play'
await sendPing(e.target, 'Click', e.target.src)
})
})
}
window.sessionStorage.setItem("gaInit", true);
window.sessionStorage.setItem('gaInit', true)
}
function isGoogleAnalyticsAvailable() {
return (typeof(ga) !== "undefined");
function isGoogleAnalyticsAvailable () {
return (typeof (ga) !== 'undefined')
}
(() => {
const win = window;
const winLocationSearch = win.location.search;
const win = window
const winLocationSearch = win.location.search
let winLocation = win.location;
let winLocation = win.location
// Check for DoNotTrack header before running GA script
if (!_dntEnabled()) {
(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments);},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m);
})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
(function (i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r; i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date(); a = s.createElement(o),
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga')
}
// Remove token and hash values from URL so that they aren't sent to GA
if (winLocationSearch.includes("token=") || winLocationSearch.includes("hash=")) {
winLocation = winLocation.toString().replace(/[?&]token=[A-Za-z0-9_-]+/, "").replace(/&hash=[A-Za-z0-9_-]+/, "");
win.history.replaceState({}, "", winLocation);
if (winLocationSearch.includes('token=') || winLocationSearch.includes('hash=')) {
winLocation = winLocation.toString().replace(/[?&]token=[A-Za-z0-9_-]+/, '').replace(/&hash=[A-Za-z0-9_-]+/, '')
win.history.replaceState({}, '', winLocation)
}
const gaEnabled = (typeof(ga) !== "undefined");
const utmParamsInUrl = (winLocationSearch.includes("utm_"));
const gaEnabled = (typeof (ga) !== 'undefined')
const utmParamsInUrl = (winLocationSearch.includes('utm_'))
const removeUtmsFromUrl = () => {
if (utmParamsInUrl) {
win.history.replaceState({}, "", winLocation.toString().replace(/[?&]utm_.*/g, ""));
win.history.replaceState({}, '', winLocation.toString().replace(/[?&]utm_.*/g, ''))
}
};
}
// Store UTM params in session
if (utmParamsInUrl) {
saveReferringPageData(new URL(winLocation).searchParams);
saveReferringPageData(new URL(winLocation).searchParams)
}
const gaInit = new Event("gaInit");
const gaInit = new Event('gaInit')
if (gaEnabled) {
ga("create", "UA-77033033-16");
ga("set", "anonymizeIp", true);
ga("set", "dimension6", `${document.body.dataset.signedInUser}`);
ga('create', 'UA-77033033-16')
ga('set', 'anonymizeIp', true)
ga('set', 'dimension6', `${document.body.dataset.signedInUser}`)
ga("send", "pageview", {
hitCallback: function() {
removeUtmsFromUrl();
sessionStorage.removeItem("gaInit");
document.dispatchEvent(gaInit);
},
});
document.addEventListener("gaInit", (e) => {
if (sessionStorage.getItem("gaInit")) {
return;
ga('send', 'pageview', {
hitCallback: function () {
removeUtmsFromUrl()
sessionStorage.removeItem('gaInit')
document.dispatchEvent(gaInit)
}
setGAListeners();
}, false);
})
document.addEventListener('gaInit', (e) => {
if (sessionStorage.getItem('gaInit')) {
return
}
setGAListeners()
}, false)
} else {
removeUtmsFromUrl();
removeUtmsFromUrl()
}
})();
})()

Просмотреть файл

@ -1,71 +1,70 @@
"use strict";
'use strict'
// open/close signed-in user fxa menu and set tabbing
function toggleMenu (evt) {
evt.stopPropagation();
const otherFocusableEls = document.querySelectorAll("button, a:not(.fxa-menu-link), input");
const fxaMenuLinks = document.querySelectorAll(".fxa-menu-link");
evt.stopPropagation()
const otherFocusableEls = document.querySelectorAll('button, a:not(.fxa-menu-link), input')
const fxaMenuLinks = document.querySelectorAll('.fxa-menu-link')
const bodyClassList = document.body.classList;
bodyClassList.toggle("menu-open");
if (bodyClassList.contains("menu-open")) {
const bento = document.querySelector("firefox-apps");
const bodyClassList = document.body.classList
bodyClassList.toggle('menu-open')
if (bodyClassList.contains('menu-open')) {
const bento = document.querySelector('firefox-apps')
if (bento._active) {
const closeBentoEvent = new Event("close-bento-menu");
bento.dispatchEvent(closeBentoEvent);
const closeBentoEvent = new Event('close-bento-menu')
bento.dispatchEvent(closeBentoEvent)
}
document.addEventListener("bento-was-opened", toggleMenu);
window.addEventListener("click", toggleMenu);
document.addEventListener('bento-was-opened', toggleMenu)
window.addEventListener('click', toggleMenu)
otherFocusableEls.forEach(el => {
el.tabIndex = -1;
});
el.tabIndex = -1
})
fxaMenuLinks.forEach(link => {
link.tabIndex = 0;
});
return;
link.tabIndex = 0
})
return
}
otherFocusableEls.forEach(el => {
el.tabIndex = 0;
});
el.tabIndex = 0
})
fxaMenuLinks.forEach(link => {
link.tabIndex = -1;
});
link.tabIndex = -1
})
document.removeEventListener("bento-was-opened", toggleMenu);
window.removeEventListener("click", toggleMenu);
document.removeEventListener('bento-was-opened', toggleMenu)
window.removeEventListener('click', toggleMenu)
}
const avatar = document.querySelector(".avatar-wrapper");
const avatar = document.querySelector('.avatar-wrapper')
if (avatar) {
avatar.addEventListener("click", toggleMenu);
const fxaMenuLinks = document.querySelectorAll(".fxa-menu-link");
avatar.addEventListener('click', toggleMenu)
const fxaMenuLinks = document.querySelectorAll('.fxa-menu-link')
avatar.addEventListener("focus", () => {
avatar.addEventListener("keydown", (e) => {
avatar.addEventListener('focus', () => {
avatar.addEventListener('keydown', (e) => {
// open menu on space bar (keyCode:32) or enter (keyCode:13) clicks
if ([32, 13].includes(e.keyCode)) {
e.preventDefault(); // prevents page from jumping or scrolling down
e.stopImmediatePropagation();
return toggleMenu(e);
e.preventDefault() // prevents page from jumping or scrolling down
e.stopImmediatePropagation()
return toggleMenu(e)
}
// close menu on escape (keyCode:27) clicks
if (e.keyCode === 27 && document.body.classList.contains("menu-open")) {
return toggleMenu(e);
if (e.keyCode === 27 && document.body.classList.contains('menu-open')) {
return toggleMenu(e)
}
});
});
})
})
fxaMenuLinks.forEach(link => {
link.addEventListener("focus", (e) => {
link.addEventListener("keyup", (e) => {
link.addEventListener('focus', (e) => {
link.addEventListener('keyup', (e) => {
if (e.keyCode === 27) {
e.preventDefault();
e.stopImmediatePropagation();
toggleMenu(e);
e.preventDefault()
e.stopImmediatePropagation()
toggleMenu(e)
}
});
});
});
})
})
})
}

Просмотреть файл

@ -1,4 +1,4 @@
"use strict";
'use strict'
/* global sendPing */
/* global getFxaUtms */
@ -6,324 +6,320 @@
/* global sendRecommendationPings */
/* global ga */
if (typeof TextEncoder === "undefined") {
const cryptoScript = document.createElement("script");
const scripts = document.getElementsByTagName("script")[0];
cryptoScript.src = "/dist/edge.min.js";
scripts.parentNode.insertBefore(cryptoScript, scripts);
if (typeof TextEncoder === 'undefined') {
const cryptoScript = document.createElement('script')
const scripts = document.getElementsByTagName('script')[0]
cryptoScript.src = '/dist/edge.min.js'
scripts.parentNode.insertBefore(cryptoScript, scripts)
}
function findAncestor(el, cls) {
function findAncestor (el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
return el
}
function toggleEl(e) {
const toggleButton = e.target;
const toggleParent = findAncestor(toggleButton, "toggle-parent");
["inactive", "active"].forEach(className => {
toggleParent.classList.toggle(className);
});
function toggleEl (e) {
const toggleButton = e.target
const toggleParent = findAncestor(toggleButton, 'toggle-parent');
['inactive', 'active'].forEach(className => {
toggleParent.classList.toggle(className)
})
}
function isValidEmail(val) {
function isValidEmail (val) {
// https://stackoverflow.com/a/46181
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(val).toLowerCase());
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(String(val).toLowerCase())
}
function getSubmittedEmail() {
const email = document.querySelector("#scan-user-email input[type=email]").value;
function getSubmittedEmail () {
const email = document.querySelector('#scan-user-email input[type=email]').value
if (!isValidEmail(email)) {
return false;
return false
}
return email;
return email
}
function overwriteLastScannedEmail(email, scannedEmailId) {
function overwriteLastScannedEmail (email, scannedEmailId) {
if (!sessionStorage) {
throw new Error("Session storage not available");
throw new Error('Session storage not available')
}
sessionStorage.removeItem("lastScannedEmail");
sessionStorage.setItem("lastScannedEmail", email);
scannedEmailId.value = sessionStorage.length;
sessionStorage.removeItem('lastScannedEmail')
sessionStorage.setItem('lastScannedEmail', email)
scannedEmailId.value = sessionStorage.length
}
function doOauth(el, { emailWatch = false } = {}) {
let url = new URL("/oauth/init", document.body.dataset.serverUrl);
function doOauth (el, { emailWatch = false } = {}) {
let url = new URL('/oauth/init', document.body.dataset.serverUrl)
url = getFxaUtms(url);
["flowId", "flowBeginTime", "entrypoint", "entrypoint_experiment", "entrypoint_variation", "form_type"].forEach(key => {
['flowId', 'flowBeginTime', 'entrypoint', 'entrypoint_experiment', 'entrypoint_variation', 'form_type'].forEach(key => {
if (el.dataset[key]) {
url.searchParams.append(key, encodeURIComponent(el.dataset[key]));
url.searchParams.append(key, encodeURIComponent(el.dataset[key]))
}
});
})
if (!sessionStorage) {
window.location.assign(url);
return;
window.location.assign(url)
return
}
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
const lastScannedEmail = sessionStorage.getItem('lastScannedEmail')
if (typeof emailWatch !== "boolean") {
throw new Error("invalid argument option in doOauth");
if (typeof emailWatch !== 'boolean') {
throw new Error('invalid argument option in doOauth')
}
if (!emailWatch) {
// Preserve entire control function
if (lastScannedEmail) {
url.searchParams.append("email", lastScannedEmail);
url.searchParams.append('email', lastScannedEmail)
}
window.location.assign(url);
return;
window.location.assign(url)
return
}
const submittedEmail = getSubmittedEmail();
const scannedEmailId = document.querySelector("#scan-user-email input[name=scannedEmailId]");
const submittedEmail = getSubmittedEmail()
const scannedEmailId = document.querySelector('#scan-user-email input[name=scannedEmailId]')
if (lastScannedEmail === submittedEmail) {
url.searchParams.append("email", lastScannedEmail);
window.location.assign(url);
return;
url.searchParams.append('email', lastScannedEmail)
window.location.assign(url)
return
}
// Use the email address the user submitted in FxA Oauth flow
overwriteLastScannedEmail(submittedEmail, scannedEmailId);
url.searchParams.append("email", submittedEmail);
window.location.assign(url);
overwriteLastScannedEmail(submittedEmail, scannedEmailId)
url.searchParams.append('email', submittedEmail)
window.location.assign(url)
}
function addFormListeners() {
function addFormListeners () {
Array.from(document.forms).forEach(form => {
if (form.querySelector("input[type=email]")) {
const emailInput = form.querySelector("input[type=email]");
emailInput.addEventListener("keydown", (e) => {
form.classList.remove("invalid");
});
if (form.querySelector('input[type=email]')) {
const emailInput = form.querySelector('input[type=email]')
emailInput.addEventListener('keydown', (e) => {
form.classList.remove('invalid')
})
emailInput.addEventListener("invalid", (e) => {
e.preventDefault();
form.classList.add("invalid");
});
emailInput.addEventListener('invalid', (e) => {
e.preventDefault()
form.classList.add('invalid')
})
emailInput.addEventListener("keydown", () => {
if (emailInput.value === "") {
sendPing(form, "Engage");
emailInput.addEventListener('keydown', () => {
if (emailInput.value === '') {
sendPing(form, 'Engage')
}
});
})
emailInput.addEventListener("focus", () => {
if (emailInput.value === "") {
sendPing(form, "Engage");
emailInput.addEventListener('focus', () => {
if (emailInput.value === '') {
sendPing(form, 'Engage')
}
});
})
}
form.addEventListener("submit", (e) => handleFormSubmits(e), true);
});
form.addEventListener('submit', (e) => handleFormSubmits(e), true)
})
}
function handleFormSubmits(formEvent) {
formEvent.preventDefault();
const thisForm = formEvent.target;
let email = "";
function handleFormSubmits (formEvent) {
formEvent.preventDefault()
const thisForm = formEvent.target
let email = ''
sendPing(thisForm, "Submit", null, { transport: "beacon" });
sendPing(thisForm, 'Submit', null, { transport: 'beacon' })
if (thisForm.email) {
email = thisForm.email.value.trim();
thisForm.email.value = email;
email = thisForm.email.value.trim()
thisForm.email.value = email
}
const formClassList = thisForm.classList;
const formClassList = thisForm.classList
if (thisForm.email && !isValidEmail(email)) {
sendPing(thisForm, "Failure");
formClassList.add("invalid");
return;
sendPing(thisForm, 'Failure')
formClassList.add('invalid')
return
}
if (formClassList.contains("email-scan")) {
hashEmailAndSend(formEvent);
return;
if (formClassList.contains('email-scan')) {
hashEmailAndSend(formEvent)
return
}
// if the form contains the class "loading-data", it has
// already been submitted, so return without re-submitting.
if (formClassList.contains("loading-data")) {
return;
if (formClassList.contains('loading-data')) {
return
}
formClassList.add("loading-data");
return thisForm.submit();
formClassList.add('loading-data')
return thisForm.submit()
}
//re-enables inputs and clears loader
function restoreInputs() {
// re-enables inputs and clears loader
function restoreInputs () {
Array.from(document.forms).forEach(form => {
form.classList.remove("loading-data");
form.classList.remove("invalid");
});
document.querySelectorAll("input").forEach(input => {
form.classList.remove('loading-data')
form.classList.remove('invalid')
})
document.querySelectorAll('input').forEach(input => {
if (input.disabled) {
input.disabled = false;
input.disabled = false
}
});
})
}
function toggleDropDownMenu(dropDownMenu) {
if (dropDownMenu.classList.contains("mobile-menu-open")) {
return dropDownMenu.classList.remove("mobile-menu-open");
function toggleDropDownMenu (dropDownMenu) {
if (dropDownMenu.classList.contains('mobile-menu-open')) {
return dropDownMenu.classList.remove('mobile-menu-open')
}
return dropDownMenu.classList.add("mobile-menu-open");
return dropDownMenu.classList.add('mobile-menu-open')
}
function toggleArticles() {
const windowWidth = window.innerWidth;
const articleToggles = document.querySelectorAll(".st-toggle-wrapper, .relay-info.toggle-parent");
function toggleArticles () {
const windowWidth = window.innerWidth
const articleToggles = document.querySelectorAll('.st-toggle-wrapper, .relay-info.toggle-parent')
if (windowWidth > 600) {
articleToggles.forEach(toggle => {
toggle.classList.add("active");
toggle.classList.remove("inactive");
});
return;
toggle.classList.add('active')
toggle.classList.remove('inactive')
})
return
}
articleToggles.forEach(toggle => {
toggle.classList.remove("active");
toggle.classList.add("inactive");
});
toggle.classList.remove('active')
toggle.classList.add('inactive')
})
}
function hideShowNavBars(win, navBar, bentoButton) {
function hideShowNavBars (win, navBar, bentoButton) {
win.onscroll = function (e) {
// catch a window that has resized from less than 600px
// to greater than 600px and unhide navigation.
if (win.innerWidth > 600) {
navBar.classList = ["show-nav-bars"];
return;
navBar.classList = ['show-nav-bars']
return
}
if (win.pageYOffset < 100) {
navBar.classList = ["show-nav-bars"];
return;
navBar.classList = ['show-nav-bars']
return
}
if (
this.oldScroll < (this.scrollY - 50) &&
navBar.classList.contains("show-nav-bars") &&
navBar.classList.contains('show-nav-bars') &&
!bentoButton._active
) {
navBar.classList = ["hide-nav-bars"];
this.oldScroll = this.scrollY;
return;
navBar.classList = ['hide-nav-bars']
this.oldScroll = this.scrollY
return
}
if (this.oldScroll > this.scrollY + 50) {
navBar.classList = ["show-nav-bars"];
this.oldScroll = this.scrollY;
return;
navBar.classList = ['show-nav-bars']
this.oldScroll = this.scrollY
return
}
this.oldScroll = this.scrollY;
};
this.oldScroll = this.scrollY
}
}
function toggleMobileFeatures(topNavBar) {
const win = window;
const windowWidth = win.innerWidth;
function toggleMobileFeatures (topNavBar) {
const win = window
const windowWidth = win.innerWidth
if (windowWidth > 800) {
const emailCards = document.querySelectorAll(".breaches-dash.email-card:not(.zero-breaches)");
const emailCards = document.querySelectorAll('.breaches-dash.email-card:not(.zero-breaches)')
emailCards.forEach(card => {
card.classList.add("active");
});
return;
card.classList.add('active')
})
return
}
const bentoButton = document.querySelector("firefox-apps");
const closeActiveEmailCards = document.querySelectorAll(".breaches-dash.email-card.active");
const bentoButton = document.querySelector('firefox-apps')
const closeActiveEmailCards = document.querySelectorAll('.breaches-dash.email-card.active')
closeActiveEmailCards.forEach(card => {
card.classList.remove("active");
});
card.classList.remove('active')
})
if (windowWidth < 600) {
hideShowNavBars(win, topNavBar, bentoButton);
addBentoObserver();
hideShowNavBars(win, topNavBar, bentoButton)
addBentoObserver()
}
}
function toggleHeaderStates(header, win) {
function toggleHeaderStates (header, win) {
if (win.outerWidth < 600) {
return;
return
}
if (win.pageYOffset > 400) {
header.classList.add("show-shadow");
header.classList.add('show-shadow')
} else {
header.classList.remove("show-shadow");
header.classList.remove('show-shadow')
}
}
function addMainNavListeners() {
const inactiveNavLinks = document.querySelectorAll(".nav-link:not(.active-link)");
function addMainNavListeners () {
const inactiveNavLinks = document.querySelectorAll('.nav-link:not(.active-link)')
inactiveNavLinks.forEach(link => {
/* Remove the .active-link-underline class from any link
that isn't the current ".active-link" which occasionally
happens when the user navigates to a page using browser
backwards/forwards buttons. */
if (link.classList.contains("active-link-underline")) {
link.classList.remove("active-link-underline");
if (link.classList.contains('active-link-underline')) {
link.classList.remove('active-link-underline')
}
link.addEventListener("mouseenter", () => {
link.classList.add("active-link-underline");
});
link.addEventListener("mouseleave", () => {
link.classList.remove("active-link-underline");
});
});
link.addEventListener('mouseenter', () => {
link.classList.add('active-link-underline')
})
link.addEventListener('mouseleave', () => {
link.classList.remove('active-link-underline')
})
})
}
function addBentoObserver() {
const bodyClasses = document.body.classList;
const bentoButton = document.querySelector("firefox-apps");
const observerConfig = { attributes: true };
function addBentoObserver () {
const bodyClasses = document.body.classList
const bentoButton = document.querySelector('firefox-apps')
const observerConfig = { attributes: true }
const watchBentoChanges = function (bentoEl, observer) {
for (const mutation of bentoEl) {
if (mutation.type === "attributes") {
bodyClasses.toggle("bento-open", bentoButton._active);
if (mutation.type === 'attributes') {
bodyClasses.toggle('bento-open', bentoButton._active)
}
}
};
}
if (bentoButton) {
const observer = new MutationObserver(watchBentoChanges);
observer.observe(bentoButton, observerConfig);
const observer = new MutationObserver(watchBentoChanges)
observer.observe(bentoButton, observerConfig)
}
}
function setHeaderHeight() {
const header = document.getElementById("header");
const height = header?.offsetHeight || 0;
function setHeaderHeight () {
const header = document.getElementById('header')
const height = header?.offsetHeight || 0
document.body.style.setProperty("--header-height", `${height}px`);
document.body.style.setProperty('--header-height', `${height}px`)
}
function recruitmentLogic() {
const recruitmentBannerLink = document.querySelector("#recruitment-banner");
function recruitmentLogic () {
const recruitmentBannerLink = document.querySelector('#recruitment-banner')
if (!recruitmentBannerLink) {
return;
return
}
const recruited = document.cookie.split("; ").some((item) => item.trim().startsWith("recruited="));
const recruited = document.cookie.split('; ').some((item) => item.trim().startsWith('recruited='))
if (recruited) {
recruitmentBannerLink.parentElement.remove();
return;
recruitmentBannerLink.parentElement.remove()
return
} else {
recruitmentBannerLink.removeAttribute("hidden");
recruitmentBannerLink.removeAttribute('hidden')
}
recruitmentBannerLink.addEventListener("click", () => {
const date = new Date();
date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000);
document.cookie = "recruited=true; expires=" + date.toUTCString();
});
recruitmentBannerLink.addEventListener('click', () => {
const date = new Date()
date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000)
document.cookie = 'recruited=true; expires=' + date.toUTCString()
})
}
// function addWaitlistSignupButtonListeners() {
@ -366,7 +362,6 @@ function recruitmentLogic() {
// const availableIntersectionObserver = ("IntersectionObserver" in window);
// const gaAvailable = typeof (ga) !== undefined;
// if (availableIntersectionObserver && gaAvailable) {
// const sendWaitlistViewPing = elemData => {
// if (elemData.userIsSignedUp === "true") {
@ -403,251 +398,246 @@ function recruitmentLogic() {
// }
// }
async function initVpnBanner() {
const vpnBanner = document.querySelector(".vpn-banner");
async function initVpnBanner () {
const vpnBanner = document.querySelector('.vpn-banner')
if (!vpnBanner) return;
if (!vpnBanner) return
const resizeObserver = new ResizeObserver(entries => updateHeight(entries[0].contentRect.height));
resizeObserver.observe(vpnBanner); // call before `await` for initial height render
const resizeObserver = new ResizeObserver(entries => updateHeight(entries[0].contentRect.height))
resizeObserver.observe(vpnBanner) // call before `await` for initial height render
const locationDataReq = new Request("/iplocation");
const protectionDataReq = new Request("https://am.i.mullvad.net/json");
const cache = await initCache();
const locationDataReq = new Request('/iplocation')
const protectionDataReq = new Request('https://am.i.mullvad.net/json')
const cache = await initCache()
const locationData = await fetch(locationDataReq)
.then(res => res.json())
.catch(e => console.warn("Error fetching location data.", e));
.catch(e => console.warn('Error fetching location data.', e))
let protectionData = await getCacheData(protectionDataReq);
let protectionData = await getCacheData(protectionDataReq)
if (!protectionData || protectionData.ip !== locationData?.clientIp) {
// get fresh data if none cached or user IP changed since last cached response
protectionData = await fetchData(protectionDataReq).then(data => {
if (!data) return null;
return { ip: data.ip, isProtected: data.mullvad_exit_ip };
});
if (!data) return null
return { ip: data.ip, isProtected: data.mullvad_exit_ip }
})
}
if (locationData?.clientIp) {
vpnBanner.querySelector(".client-ip output").textContent = locationData.clientIp;
vpnBanner.querySelector('.client-ip output').textContent = locationData.clientIp
} else {
vpnBanner.querySelector(".client-ip").remove();
vpnBanner.querySelector('.client-ip').remove()
}
if (locationData?.shortLocation) {
vpnBanner.querySelector(".short-location output").textContent = locationData.shortLocation;
vpnBanner.querySelector('.short-location output').textContent = locationData.shortLocation
} else {
vpnBanner.querySelector(".short-location").remove();
vpnBanner.querySelector('.short-location').remove()
}
if (locationData?.fullLocation) {
vpnBanner.querySelector(".full-location output").textContent = locationData.fullLocation;
vpnBanner.querySelector('.full-location output').textContent = locationData.fullLocation
} else {
vpnBanner.querySelector(".full-location").remove();
vpnBanner.querySelector('.full-location').remove()
}
vpnBanner.cta = vpnBanner.querySelector("a.vpn-banner-cta");
vpnBanner.cta.setAttribute("href", vpnBanner.cta.getAttribute("href") + getPageAttribution());
vpnBanner.setAttribute("data-protected", Boolean(protectionData?.isProtected));
vpnBanner.addEventListener("click", handleClick);
vpnBanner.cta = vpnBanner.querySelector('a.vpn-banner-cta')
vpnBanner.cta.setAttribute('href', vpnBanner.cta.getAttribute('href') + getPageAttribution())
vpnBanner.setAttribute('data-protected', Boolean(protectionData?.isProtected))
vpnBanner.addEventListener('click', handleClick)
if (cache && protectionData) cache.put(protectionDataReq, new Response(JSON.stringify(protectionData)));
if (cache && protectionData) cache.put(protectionDataReq, new Response(JSON.stringify(protectionData)))
async function initCache() {
const cacheAvailable = "caches" in self;
let cache;
async function initCache () {
const cacheAvailable = 'caches' in self
let cache
if (cacheAvailable) cache = await caches.open("vpn-banner").catch(e => null);
if (cacheAvailable) cache = await caches.open('vpn-banner').catch(e => null)
return cache;
return cache
}
async function getCacheData(req) {
if (!cache) return null;
async function getCacheData (req) {
if (!cache) return null
const json = await cache.match(req)
.then(res => res.json())
.catch(e => console.warn("Could not get cached response.", e.message));
.catch(e => console.warn('Could not get cached response.', e.message))
return json;
return json
}
async function fetchData(req, reqTimeoutMs = 4000) {
const abortController = new AbortController();
const timer = setTimeout(() => abortController.abort(), reqTimeoutMs); // abort a delayed response
async function fetchData (req, reqTimeoutMs = 4000) {
const abortController = new AbortController()
const timer = setTimeout(() => abortController.abort(), reqTimeoutMs) // abort a delayed response
const json = await fetch(req, { signal: abortController.signal })
.then(res => {
clearTimeout(timer);
if (!res.ok) throw new Error(`Bad response (${res.status})`);
return res.json();
clearTimeout(timer)
if (!res.ok) throw new Error(`Bad response (${res.status})`)
return res.json()
})
.catch(e => console.warn("Error fetching protection data.", e));
.catch(e => console.warn('Error fetching protection data.', e))
return json;
return json
}
function getPageAttribution() {
let page = location.pathname;
function getPageAttribution () {
let page = location.pathname
if (page.startsWith("/")) page = page.slice(1);
if (page.startsWith('/')) page = page.slice(1)
if (page === "") page = "home";
if (page === '') page = 'home'
return `&utm_content=${page}`;
return `&utm_content=${page}`
}
function handleClick(e) {
function handleClick (e) {
switch (e.target.className) {
case "vpn-banner-top":
case "vpn-banner-close":
vpnBanner.toggleAttribute("data-expanded");
break;
case 'vpn-banner-top':
case 'vpn-banner-close':
vpnBanner.toggleAttribute('data-expanded')
break
}
}
function updateHeight(h) {
document.body.style.setProperty("--vpn-banner-height", `${Math.floor(h)}px`);
function updateHeight (h) {
document.body.style.setProperty('--vpn-banner-height', `${Math.floor(h)}px`)
}
}
async function initCsatBanner() {
const csatBanner = document.querySelector(".csat-banner");
async function initCsatBanner () {
const csatBanner = document.querySelector('.csat-banner')
if (!csatBanner) return;
if (!csatBanner) return
csatBanner.addEventListener("click", handleEvent);
csatBanner.addEventListener('click', handleEvent)
function handleEvent(e) {
const ttl = new Date();
ttl.setDate(ttl.getDate() + 90);
document.cookie = "csatHidden=1; path=/; sameSite=Lax; expires=" + ttl.toUTCString();
function handleEvent (e) {
const ttl = new Date()
ttl.setDate(ttl.getDate() + 90)
document.cookie = 'csatHidden=1; path=/; sameSite=Lax; expires=' + ttl.toUTCString()
switch (e.target.name) {
case "csat-close-btn":
csatBanner.toggleAttribute("hidden", true);
csatBanner.removeEventListener("click", handleEvent);
break;
case "csat-option":
csatBanner.toggleAttribute("disabled", true);
csatBanner.querySelector(".csat-question").textContent = "Thanks for your feedback!";
e.target.parentElement.classList.add("selected");
if (window.ga) ga("send", "event", "CSAT banner", "submit", e.target.nextSibling.textContent, e.target.value);
break;
case 'csat-close-btn':
csatBanner.toggleAttribute('hidden', true)
csatBanner.removeEventListener('click', handleEvent)
break
case 'csat-option':
csatBanner.toggleAttribute('disabled', true)
csatBanner.querySelector('.csat-question').textContent = 'Thanks for your feedback!'
e.target.parentElement.classList.add('selected')
if (window.ga) ga('send', 'event', 'CSAT banner', 'submit', e.target.nextSibling.textContent, e.target.value)
break
}
setHeaderHeight();
setHeaderHeight()
}
}
(async () => {
document.addEventListener("touchstart", function () { }, true);
const win = window;
const header = document.getElementById("header");
const topNavigation = document.querySelector("#navigation-wrapper");
document.addEventListener('touchstart', function () { }, true)
const win = window
const header = document.getElementById('header')
const topNavigation = document.querySelector('#navigation-wrapper')
win.addEventListener("pageshow", function () {
addMainNavListeners();
toggleMobileFeatures(topNavigation);
toggleArticles();
toggleHeaderStates(header, win);
document.forms ? (restoreInputs(), addFormListeners()) : null;
});
win.addEventListener('pageshow', function () {
addMainNavListeners()
toggleMobileFeatures(topNavigation)
toggleArticles()
toggleHeaderStates(header, win)
document.forms ? (restoreInputs(), addFormListeners()) : null
})
document.forms ? (restoreInputs(), addFormListeners()) : null;
document.forms ? (restoreInputs(), addFormListeners()) : null
let windowWidth = win.outerWidth;
document.addEventListener("DOMContentLoaded", setHeaderHeight);
win.addEventListener("resize", () => {
const newWindowWidth = win.outerWidth;
let windowWidth = win.outerWidth
document.addEventListener('DOMContentLoaded', setHeaderHeight)
win.addEventListener('resize', () => {
const newWindowWidth = win.outerWidth
if (newWindowWidth !== windowWidth) {
toggleMobileFeatures(topNavigation);
toggleArticles();
windowWidth = newWindowWidth;
setHeaderHeight();
toggleMobileFeatures(topNavigation)
toggleArticles()
windowWidth = newWindowWidth
setHeaderHeight()
}
});
})
document.addEventListener("scroll", () => toggleHeaderStates(header, win));
document.addEventListener('scroll', () => toggleHeaderStates(header, win))
document.querySelectorAll(".breach-logo:not(.lazy-img)").forEach(logo => {
logo.addEventListener("error", (missingLogo) => {
missingLogo.target.src = "/img/svg/placeholder.svg";
});
});
document.querySelectorAll('.breach-logo:not(.lazy-img)').forEach(logo => {
logo.addEventListener('error', (missingLogo) => {
missingLogo.target.src = '/img/svg/placeholder.svg'
})
})
document.querySelectorAll(".toggle").forEach(toggle => {
toggle.addEventListener("click", toggleEl);
});
document.querySelectorAll('.toggle').forEach(toggle => {
toggle.addEventListener('click', toggleEl)
})
document.querySelectorAll(".open-oauth").forEach(button => {
button.addEventListener("click", (e) => doOauth(e.target));
});
document.querySelectorAll('.open-oauth').forEach(button => {
button.addEventListener('click', (e) => doOauth(e.target))
})
document.querySelectorAll("#see-additional-recs").forEach(button => {
button.addEventListener("click", () => {
button.classList.add("fade-out");
const overflowRecs = document.getElementById("overflow-recs");
overflowRecs.classList.remove("hide");
if (typeof (ga) !== "undefined") {
document.querySelectorAll('#see-additional-recs').forEach(button => {
button.addEventListener('click', () => {
button.classList.add('fade-out')
const overflowRecs = document.getElementById('overflow-recs')
overflowRecs.classList.remove('hide')
if (typeof (ga) !== 'undefined') {
// Send "Click" ping for #see-additional-recs click
ga("send", "event", "Breach Details: See Additional Recommendations", "Click", "See Additional Recommendations");
ga('send', 'event', 'Breach Details: See Additional Recommendations', 'Click', 'See Additional Recommendations')
// Send "View" pings for any CTAs that become visible on #see-additional-recs click
sendRecommendationPings(".overflow-rec-cta");
sendRecommendationPings('.overflow-rec-cta')
}
});
});
})
})
setHeaderHeight();
recruitmentLogic();
setHeaderHeight()
recruitmentLogic()
// addWaitlistSignupButtonListeners();
// addWaitlistObservers();
initVpnBanner();
initCsatBanner();
initVpnBanner()
initCsatBanner()
const dropDownMenu = document.querySelector(".mobile-nav.show-mobile");
dropDownMenu.addEventListener("click", () => toggleDropDownMenu(dropDownMenu));
const dropDownMenu = document.querySelector('.mobile-nav.show-mobile')
dropDownMenu.addEventListener('click', () => toggleDropDownMenu(dropDownMenu))
if (document.getElementById("fxaCheckbox")) {
document.getElementById("fxaCheckbox").style.display = "block";
if (document.getElementById('fxaCheckbox')) {
document.getElementById('fxaCheckbox').style.display = 'block'
}
const createFxaCheckbox = document.getElementById("createFxaCheckbox");
const submitBtn = document.querySelector(".breachesSubmitButton");
const createFxaCheckbox = document.getElementById('createFxaCheckbox')
const submitBtn = document.querySelector('.breachesSubmitButton')
if (submitBtn) {
submitBtn.addEventListener("click", (e) => {
submitBtn.addEventListener('click', (e) => {
// Email Validation
const scanForm = document.getElementById("scan-user-email");
const scanFormEmailValue = document.querySelector("#scan-user-email input[type='email']").value;
const scanForm = document.getElementById('scan-user-email')
const scanFormEmailValue = document.querySelector("#scan-user-email input[type='email']").value
if (scanFormEmailValue.length < 1 || !isValidEmail(scanFormEmailValue)) {
scanForm.classList.add("invalid");
return;
scanForm.classList.add('invalid')
return
}
if (createFxaCheckbox.checked) {
e.preventDefault();
e.preventDefault()
// Send GA Ping
if (typeof (ga) !== "undefined") {
ga("send", {
hitType: "event",
eventCategory: "Sign Up Button",
eventAction: "Engage",
eventLabel: "fx-monitor-homepage-fxa-checkbox",
if (typeof (ga) !== 'undefined') {
ga('send', {
hitType: 'event',
eventCategory: 'Sign Up Button',
eventAction: 'Engage',
eventLabel: 'fx-monitor-homepage-fxa-checkbox',
options: {
transport: "beacon",
},
});
transport: 'beacon'
}
})
}
doOauth(e.target, { emailWatch: true });
return;
doOauth(e.target, { emailWatch: true })
}
});
})
}
})();
})()

Просмотреть файл

@ -1,81 +1,78 @@
"use strict";
'use strict';
/* global ga */
/* global doOauth */
(()=> {
function trapFocusInModal(modal, trapFocusInModal=true) {
(() => {
function trapFocusInModal (modal, trapFocusInModal = true) {
const focusableEls = [
"button",
"input",
"[href]",
"[tabindex]",
];
const allFocusableEls = document.querySelectorAll(focusableEls);
const focusableModalEls = modal.querySelectorAll(focusableEls);
let allFocusableElsTabIndexValue = 0;
let focusableModalElsTabIndexValue = -1;
'button',
'input',
'[href]',
'[tabindex]'
]
const allFocusableEls = document.querySelectorAll(focusableEls)
const focusableModalEls = modal.querySelectorAll(focusableEls)
let allFocusableElsTabIndexValue = 0
let focusableModalElsTabIndexValue = -1
if (trapFocusInModal) {
allFocusableElsTabIndexValue = -1;
focusableModalElsTabIndexValue = 0;
allFocusableElsTabIndexValue = -1
focusableModalElsTabIndexValue = 0
}
const setTabIndex = (elemArray, tabIndexValue) => {
elemArray.forEach(el => {
el.tabIndex = tabIndexValue;
});
};
el.tabIndex = tabIndexValue
})
}
setTabIndex(allFocusableEls, allFocusableElsTabIndexValue);
setTabIndex(focusableModalEls, focusableModalElsTabIndexValue);
return;
setTabIndex(allFocusableEls, allFocusableElsTabIndexValue)
setTabIndex(focusableModalEls, focusableModalElsTabIndexValue)
}
function sendBreachDetailAnalyticsPing(eventCategory, eventAction, eventLabel) {
if (typeof(ga) !== "undefined") {
function sendBreachDetailAnalyticsPing (eventCategory, eventAction, eventLabel) {
if (typeof (ga) !== 'undefined') {
// Set view pings as nonInteraction:true to get accurate bounce rate
let options = {};
if (eventAction !== "Engage"){
options = {nonInteraction: true};
let options = {}
if (eventAction !== 'Engage') {
options = { nonInteraction: true }
}
ga("send", "event", eventCategory, eventAction, eventLabel, options);
ga('send', 'event', eventCategory, eventAction, eventLabel, options)
}
}
// set up IntersectionObserver to watch for event triggers on breach-details pages
// and send "View" ping when they become visible in the viewport
const resolveBtns = document.querySelectorAll(".resolve-button, a.what-to-do-next");
const productPromos = document.querySelectorAll(".product-promo-wrapper");
const gaEventTriggers = [...productPromos, ...resolveBtns];
const resolveBtns = document.querySelectorAll('.resolve-button, a.what-to-do-next')
const productPromos = document.querySelectorAll('.product-promo-wrapper')
const gaEventTriggers = [...productPromos, ...resolveBtns]
const availableIntersectionObserver = ("IntersectionObserver" in window);
const gaAvailable = typeof(ga) !== undefined;
const availableIntersectionObserver = ('IntersectionObserver' in window)
const gaAvailable = typeof (ga) !== undefined
// TODO: Store this in the dataset of breach resolution event triggers
const resolutionEventCategory = "Breach Resolution";
const resolutionEventCategory = 'Breach Resolution'
if (availableIntersectionObserver && gaEventTriggers && gaAvailable) {
const onEventTriggersComingIntoView = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const entryData = entry.target.dataset;
const analyticsLabel = entryData.analyticsLabel;
if (entry.target.classList.contains("product-promo-wrapper")) {
sendBreachDetailAnalyticsPing(entryData.eventCategory, "View", analyticsLabel);
observer.unobserve(entry.target);
return;
const entryData = entry.target.dataset
const analyticsLabel = entryData.analyticsLabel
if (entry.target.classList.contains('product-promo-wrapper')) {
sendBreachDetailAnalyticsPing(entryData.eventCategory, 'View', analyticsLabel)
observer.unobserve(entry.target)
return
}
sendBreachDetailAnalyticsPing(resolutionEventCategory, "View", analyticsLabel);
observer.unobserve(entry.target);
return;
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'View', analyticsLabel)
observer.unobserve(entry.target)
}
});
};
})
}
const observer = new IntersectionObserver(onEventTriggersComingIntoView, { rootMargin: "0px"});
const observer = new IntersectionObserver(onEventTriggersComingIntoView, { rootMargin: '0px' })
gaEventTriggers.forEach(el => {
observer.observe(el);
});
observer.observe(el)
})
}
// Fallback for older browsers without IntersectionObserver:
@ -83,85 +80,84 @@
// of whether or not the triggers are actually visible.
if (!availableIntersectionObserver && gaEventTriggers && gaAvailable) {
gaEventTriggers.forEach(el => {
const elemData = el.dataset;
if (el.classList.contains("product-promo-wrappper")) {
return sendBreachDetailAnalyticsPing(elemData.eventCategory, "View - No IntersectionObserver", elemData.analyticsLabel);
const elemData = el.dataset
if (el.classList.contains('product-promo-wrappper')) {
return sendBreachDetailAnalyticsPing(elemData.eventCategory, 'View - No IntersectionObserver', elemData.analyticsLabel)
}
sendBreachDetailAnalyticsPing(resolutionEventCategory, "View - No IntersectionObserver", elemData.analyticsLabel);
});
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'View - No IntersectionObserver', elemData.analyticsLabel)
})
}
productPromos.forEach(promo => {
promo.addEventListener("click", (e) => {
const promoData = promo.dataset;
sendBreachDetailAnalyticsPing(promoData.eventCategory, "Engage", promoData.analyticsLabel);
if (promo.classList.contains("open-oauth")) {
e.preventDefault();
doOauth(promo);
promo.addEventListener('click', (e) => {
const promoData = promo.dataset
sendBreachDetailAnalyticsPing(promoData.eventCategory, 'Engage', promoData.analyticsLabel)
if (promo.classList.contains('open-oauth')) {
e.preventDefault()
doOauth(promo)
}
});
});
})
})
resolveBtns.forEach(btn => {
btn.addEventListener("click", async(e) => {
btn.addEventListener('click', async (e) => {
if (gaAvailable) {
sendBreachDetailAnalyticsPing(resolutionEventCategory, "Engage", btn.dataset.analyticsLabel);
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'Engage', btn.dataset.analyticsLabel)
}
// If "What to do next" link is clicked, scroll to the list of recommendations.
// The default behavior for this link results in the "What to do for this breach" headline
// scrolling to the very top of the page and being covered up by the position:fixed header.
if (btn.dataset.analyticsLabel === "what-to-do-next") {
e.preventDefault();
const recommendationsWrapper = document.getElementById("what-to-do-next");
return window.scrollTo(0, recommendationsWrapper.offsetTop-100);
if (btn.dataset.analyticsLabel === 'what-to-do-next') {
e.preventDefault()
const recommendationsWrapper = document.getElementById('what-to-do-next')
return window.scrollTo(0, recommendationsWrapper.offsetTop - 100)
}
btn.classList.add("loading");
btn.classList.add('loading')
const confirmationModal = document.querySelector(".breach-resolution-modal");
confirmationModal.classList.add("modal-loading");
trapFocusInModal(confirmationModal, true);
const confirmationModal = document.querySelector('.breach-resolution-modal')
confirmationModal.classList.add('modal-loading')
trapFocusInModal(confirmationModal, true)
const resolutionData = JSON.stringify(btn.dataset);
const resolutionData = JSON.stringify(btn.dataset)
try {
const response = await fetch("/user/resolve-breach", {
const response = await fetch('/user/resolve-breach', {
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json'
},
mode: "same-origin",
method: "POST",
body: resolutionData,
});
mode: 'same-origin',
method: 'POST',
body: resolutionData
})
if (response && response.redirected === true) {
return window.location.reload(true);
return window.location.reload(true)
}
if (response) {
const { headline, headlineClassName, progressMessage, progressStatus } = await response.json();
confirmationModal.classList.add("modal-open");
const goToDashboardBtn = confirmationModal.querySelector(".go-to-dash");
goToDashboardBtn.focus();
const closeModalBtns = confirmationModal.querySelectorAll(".close-modal");
const { headline, headlineClassName, progressMessage, progressStatus } = await response.json()
confirmationModal.classList.add('modal-open')
const goToDashboardBtn = confirmationModal.querySelector('.go-to-dash')
goToDashboardBtn.focus()
const closeModalBtns = confirmationModal.querySelectorAll('.close-modal')
closeModalBtns.forEach(closeModalBtn => {
closeModalBtn.addEventListener("click", () => {
return window.location.reload();
});
});
closeModalBtn.addEventListener('click', () => {
return window.location.reload()
})
})
const modalHeadline = document.getElementById("confirmation-headline");
modalHeadline.textContent = headline;
modalHeadline.classList.add(headlineClassName);
const modalHeadline = document.getElementById('confirmation-headline')
modalHeadline.textContent = headline
modalHeadline.classList.add(headlineClassName)
const modalProgressMessage = document.getElementById("confirmation-progress-message");
modalProgressMessage.textContent = progressMessage;
const modalProgressMessage = document.getElementById('confirmation-progress-message')
modalProgressMessage.textContent = progressMessage
const modalProgressStatus = document.getElementById("modal-progress-status");
modalProgressStatus.textContent = progressStatus;
const modalProgressStatus = document.getElementById('modal-progress-status')
modalProgressStatus.textContent = progressStatus
}
} catch(e) {
} catch (e) {
// What do we want to do here?
}
});
});
})();
})
})
})()

Просмотреть файл

@ -1,46 +1,46 @@
"use strict";
'use strict'
/* eslint-disable no-unused-vars */
/* global libpolycrypt */
/* global sendPing */
async function sha1(message) {
message = message.toLowerCase();
const msgBuffer = new TextEncoder("utf-8").encode(message);
let hashBuffer;
async function sha1 (message) {
message = message.toLowerCase()
const msgBuffer = new TextEncoder('utf-8').encode(message)
let hashBuffer
if (/edge/i.test(navigator.userAgent)) {
hashBuffer = libpolycrypt.sha1(msgBuffer);
hashBuffer = libpolycrypt.sha1(msgBuffer)
} else {
hashBuffer = await crypto.subtle.digest("SHA-1", msgBuffer);
hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer)
}
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("");
return hashHex.toUpperCase();
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
return hashHex.toUpperCase()
}
async function hashEmailAndSend(emailFormSubmitEvent) {
emailFormSubmitEvent.preventDefault();
const emailForm = emailFormSubmitEvent.target;
const emailInput = document.getElementById("scan-email");
const userEmail = emailInput.value;
async function hashEmailAndSend (emailFormSubmitEvent) {
emailFormSubmitEvent.preventDefault()
const emailForm = emailFormSubmitEvent.target
const emailInput = document.getElementById('scan-email')
const userEmail = emailInput.value
// show loader and hash email
emailForm.classList.add("loading-data");
emailForm.querySelector("input[name=emailHash]").value = await sha1(userEmail);
emailForm.classList.add('loading-data')
emailForm.querySelector('input[name=emailHash]').value = await sha1(userEmail)
// set unhashed email in client's sessionStorage and send key to server
// so we can pluck these out later in scan-results and not lose them on back clicks
if (sessionStorage) {
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
const lastScannedEmail = sessionStorage.getItem('lastScannedEmail')
if (!lastScannedEmail || lastScannedEmail !== userEmail) {
sessionStorage.removeItem("lastScannedEmail");
sessionStorage.setItem("lastScannedEmail", userEmail);
emailForm.querySelector("input[name=scannedEmailId]").value = sessionStorage.length;
sessionStorage.removeItem('lastScannedEmail')
sessionStorage.setItem('lastScannedEmail', userEmail)
emailForm.querySelector('input[name=scannedEmailId]').value = sessionStorage.length
}
}
// clear input, send ping, and submit
emailInput.value = "";
sendPing(emailForm, "Success", null, {transport: "beacon"});
emailForm.submit();
emailInput.value = ''
sendPing(emailForm, 'Success', null, { transport: 'beacon' })
emailForm.submit()
}

Просмотреть файл

@ -1,8 +1,8 @@
"use strict";
'use strict';
(() => {
if (document.getElementById("scannedEmail")) {
const scannedEmail = document.getElementById("scannedEmail");
scannedEmail.textContent = sessionStorage.getItem("lastScannedEmail");
if (document.getElementById('scannedEmail')) {
const scannedEmail = document.getElementById('scannedEmail')
scannedEmail.textContent = sessionStorage.getItem('lastScannedEmail')
}
})();
})()

Просмотреть файл

@ -1,13 +1,11 @@
"use strict";
'use strict'
const express = require("express");
const { getBreachDetail } = require("../controllers/breach-details");
const { asyncMiddleware } = require("../middleware");
const express = require('express')
const { getBreachDetail } = require('../controllers/breach-details')
const { asyncMiddleware } = require('../middleware')
const router = express.Router()
const router = express.Router();
router.get('/:breachName', asyncMiddleware(getBreachDetail))
router.get("/:breachName", asyncMiddleware(getBreachDetail));
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,16 +1,12 @@
"use strict";
'use strict'
const express = require("express");
const {vers, heartbeat} = require("../controllers/dockerflow");
const express = require('express')
const { vers, heartbeat } = require('../controllers/dockerflow')
const router = express.Router()
const router = express.Router();
router.get("/__version__", vers);
router.get("/__heartbeat__", heartbeat);
router.get("/__lbheartbeat__", heartbeat);
module.exports = router;
router.get('/__version__', vers)
router.get('/__heartbeat__', heartbeat)
router.get('/__lbheartbeat__', heartbeat)
module.exports = router

Просмотреть файл

@ -1,10 +1,9 @@
"use strict";
const express = require("express");
const { getEmailMockUps, notFound } = require("../controllers/email-l10n");
const router = express.Router();
'use strict'
const express = require('express')
const { getEmailMockUps, notFound } = require('../controllers/email-l10n')
const router = express.Router()
router.get('/', getEmailMockUps)
router.use(notFound)
router.get("/", getEmailMockUps);
router.use(notFound);
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,19 +1,18 @@
"use strict";
'use strict'
const express = require("express");
const bodyParser = require("body-parser");
const express = require('express')
const bodyParser = require('body-parser')
const bearerToken = require("express-bearer-token");
const bearerToken = require('express-bearer-token')
const {asyncMiddleware} = require("../middleware");
const {notify, breaches} = require("../controllers/hibp");
const { asyncMiddleware } = require('../middleware')
const { notify, breaches } = require('../controllers/hibp')
const router = express.Router()
const jsonParser = bodyParser.json()
const router = express.Router();
const jsonParser = bodyParser.json();
router.use('/notify', bearerToken())
router.post('/notify', jsonParser, asyncMiddleware(notify))
router.get('/breaches', jsonParser, asyncMiddleware(breaches))
router.use("/notify", bearerToken());
router.post("/notify", jsonParser, asyncMiddleware(notify));
router.get("/breaches", jsonParser, asyncMiddleware(breaches));
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,34 +1,34 @@
"use strict";
'use strict'
const express = require("express");
const bodyParser = require("body-parser");
const csrf = require("csurf");
const express = require('express')
const bodyParser = require('body-parser')
const csrf = require('csurf')
const {
home, getAboutPage, getAllBreaches, getBentoStrings,
getSecurityTips, notFound, removeMyData, addEmailToWaitlist,
} = require("../controllers/home");
const { getIpLocation } = require("../controllers/ip-location");
getSecurityTips, notFound, removeMyData, addEmailToWaitlist
} = require('../controllers/home')
const { getIpLocation } = require('../controllers/ip-location')
const { getShareUTMs, requireSessionUser } = require("../middleware");
const { getShareUTMs, requireSessionUser } = require('../middleware')
const csrfProtection = csrf();
const jsonParser = bodyParser.json();
const router = express.Router();
const csrfProtection = csrf()
const jsonParser = bodyParser.json()
const router = express.Router()
router.get("/", csrfProtection, home);
router.get("/share/orange", csrfProtection, getShareUTMs, home);
router.get("/share/purple", csrfProtection, getShareUTMs, home);
router.get("/share/blue", csrfProtection, getShareUTMs, home);
router.get("/share/:breach", csrfProtection, getShareUTMs, home);
router.get("/share/", csrfProtection, getShareUTMs, home);
router.get("/about", getAboutPage);
router.get("/breaches", getAllBreaches);
router.get("/security-tips", getSecurityTips);
router.get("/getBentoStrings", getBentoStrings);
router.get("/remove-my-data", requireSessionUser, removeMyData);
router.post("/join-waitlist", jsonParser, requireSessionUser, addEmailToWaitlist);
router.get("/iplocation", getIpLocation);
router.use(notFound);
router.get('/', csrfProtection, home)
router.get('/share/orange', csrfProtection, getShareUTMs, home)
router.get('/share/purple', csrfProtection, getShareUTMs, home)
router.get('/share/blue', csrfProtection, getShareUTMs, home)
router.get('/share/:breach', csrfProtection, getShareUTMs, home)
router.get('/share/', csrfProtection, getShareUTMs, home)
router.get('/about', getAboutPage)
router.get('/breaches', getAllBreaches)
router.get('/security-tips', getSecurityTips)
router.get('/getBentoStrings', getBentoStrings)
router.get('/remove-my-data', requireSessionUser, removeMyData)
router.post('/join-waitlist', jsonParser, requireSessionUser, addEmailToWaitlist)
router.get('/iplocation', getIpLocation)
router.use(notFound)
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,16 +1,15 @@
"use strict";
'use strict'
const express = require("express");
const bodyParser = require("body-parser");
const express = require('express')
const bodyParser = require('body-parser')
const {asyncMiddleware} = require("../middleware");
const {init, confirmed} = require("../controllers/oauth");
const { asyncMiddleware } = require('../middleware')
const { init, confirmed } = require('../controllers/oauth')
const router = express.Router()
const jsonParser = bodyParser.json()
const router = express.Router();
const jsonParser = bodyParser.json();
router.get('/init', jsonParser, init)
router.get('/confirmed', jsonParser, asyncMiddleware(confirmed))
router.get("/init", jsonParser, init);
router.get("/confirmed", jsonParser, asyncMiddleware(confirmed));
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,18 +1,17 @@
"use strict";
'use strict'
const express = require("express");
const bodyParser = require("body-parser");
const csrf = require("csurf");
const express = require('express')
const bodyParser = require('body-parser')
const csrf = require('csurf')
const { asyncMiddleware } = require("../middleware");
const { post, get } = require("../controllers/scan");
const { asyncMiddleware } = require('../middleware')
const { post, get } = require('../controllers/scan')
const router = express.Router();
const urlEncodedParser = bodyParser.urlencoded({ extended: false });
const csrfProtection = csrf();
const router = express.Router()
const urlEncodedParser = bodyParser.urlencoded({ extended: false })
const csrfProtection = csrf()
router.post('/', urlEncodedParser, csrfProtection, asyncMiddleware(post))
router.get('/', get)
router.post("/", urlEncodedParser, csrfProtection, asyncMiddleware(post));
router.get("/", get);
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,23 +1,22 @@
"use strict";
'use strict'
const bodyParser = require("body-parser");
const express = require("express");
const bodyParser = require('body-parser')
const express = require('express')
const AppConstants = require("../app-constants");
const mozlog = require("../log");
const {notification} = require("../controllers/ses");
const AppConstants = require('../app-constants')
const mozlog = require('../log')
const { notification } = require('../controllers/ses')
const log = mozlog("routes.ses");
const router = express.Router();
const textParser = bodyParser.text();
const log = mozlog('routes.ses')
const router = express.Router()
const textParser = bodyParser.text()
if (AppConstants.SES_NOTIFICATION_LOG_ONLY) {
router.post("/notification", textParser, (req, res, next) => {
log.info("ses-notification-body", { body: req.body });
});
router.post('/notification', textParser, (req, res, next) => {
log.info('ses-notification-body', { body: req.body })
})
} else {
router.post("/notification", textParser, notification);
router.post('/notification', textParser, notification)
}
module.exports = router;
module.exports = router

Просмотреть файл

@ -1,38 +1,37 @@
"use strict";
'use strict'
const express = require("express");
const bearerToken = require("express-bearer-token");
const bodyParser = require("body-parser");
const csrf = require("csurf");
const express = require('express')
const bearerToken = require('express-bearer-token')
const bodyParser = require('body-parser')
const csrf = require('csurf')
const { asyncMiddleware, requireSessionUser } = require("../middleware");
const { asyncMiddleware, requireSessionUser } = require('../middleware')
const {
add, verify, logout,
getDashboard, getPreferences, getBreachStats,
removeEmail, resendEmail, updateCommunicationOptions,
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm, postResolveBreach,
} = require("../controllers/user");
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm, postResolveBreach
} = require('../controllers/user')
const router = express.Router();
const jsonParser = bodyParser.json();
const urlEncodedParser = bodyParser.urlencoded({ extended: false });
const csrfProtection = csrf();
const router = express.Router()
const jsonParser = bodyParser.json()
const urlEncodedParser = bodyParser.urlencoded({ extended: false })
const csrfProtection = csrf()
router.get("/dashboard", csrfProtection, requireSessionUser, asyncMiddleware(getDashboard));
router.get("/preferences", csrfProtection, requireSessionUser, asyncMiddleware(getPreferences));
router.use("/breach-stats", bearerToken());
router.get("/breach-stats", urlEncodedParser, asyncMiddleware(getBreachStats));
router.get("/logout", logout);
router.post("/email", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(add));
router.post("/remove-email", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(removeEmail));
router.post("/resend-email", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(resendEmail));
router.post("/update-comm-option", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(updateCommunicationOptions));
router.get("/verify", jsonParser, asyncMiddleware(verify));
router.use("/unsubscribe", urlEncodedParser);
router.get("/unsubscribe", urlEncodedParser, asyncMiddleware(getUnsubscribe));
router.post("/unsubscribe", csrfProtection, asyncMiddleware(postUnsubscribe));
router.get("/remove-fxm", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(getRemoveFxm));
router.post("/remove-fxm", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(postRemoveFxm));
router.post("/resolve-breach", jsonParser, urlEncodedParser, requireSessionUser, asyncMiddleware(postResolveBreach));
module.exports = router;
router.get('/dashboard', csrfProtection, requireSessionUser, asyncMiddleware(getDashboard))
router.get('/preferences', csrfProtection, requireSessionUser, asyncMiddleware(getPreferences))
router.use('/breach-stats', bearerToken())
router.get('/breach-stats', urlEncodedParser, asyncMiddleware(getBreachStats))
router.get('/logout', logout)
router.post('/email', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(add))
router.post('/remove-email', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(removeEmail))
router.post('/resend-email', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(resendEmail))
router.post('/update-comm-option', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(updateCommunicationOptions))
router.get('/verify', jsonParser, asyncMiddleware(verify))
router.use('/unsubscribe', urlEncodedParser)
router.get('/unsubscribe', urlEncodedParser, asyncMiddleware(getUnsubscribe))
router.post('/unsubscribe', csrfProtection, asyncMiddleware(postUnsubscribe))
router.get('/remove-fxm', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(getRemoveFxm))
router.post('/remove-fxm', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(postRemoveFxm))
router.post('/resolve-breach', jsonParser, urlEncodedParser, requireSessionUser, asyncMiddleware(postResolveBreach))
module.exports = router

Просмотреть файл

@ -1,91 +1,90 @@
"use strict";
'use strict'
const { URL } = require("url");
const { URL } = require('url')
const HIBP = require("./hibp");
const sha1 = require("./sha1-utils");
const HIBP = require('./hibp')
const sha1 = require('./sha1-utils')
const AppConstants = require("./app-constants");
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
const { getExperimentFlags } = require("./controllers/utils");
const AppConstants = require('./app-constants')
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
const { getExperimentFlags } = require('./controllers/utils')
const scanResult = async(req, selfScan=false) => {
const scanResult = async (req, selfScan = false) => {
const allBreaches = req.app.locals.breaches
let scannedEmail = null
const allBreaches = req.app.locals.breaches;
let scannedEmail = null;
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
const title = req.fluentFormat("scan-title");
let foundBreaches = [];
let specificBreach = null;
let doorhangerScan = false;
let userCompromised = false;
let signedInUser = null;
let fullReport = false;
let userDash = false;
let scannedEmailId = null;
const title = req.fluentFormat('scan-title')
let foundBreaches = []
let specificBreach = null
let doorhangerScan = false
let userCompromised = false
let signedInUser = null
let fullReport = false
let userDash = false
let scannedEmailId = null
if (req.session.user) {
signedInUser = req.session.user;
signedInUser = req.session.user
}
// Checks if the user scanning their own verified email.
if (req.body && req.body.emailHash) {
scannedEmail = req.body.emailHash;
scannedEmail = req.body.emailHash
if (req.body.scannedEmailId) {
scannedEmailId = req.body.scannedEmailId;
scannedEmailId = req.body.scannedEmailId
}
if (signedInUser) {
for (const emailAddress of signedInUser.email_addresses) {
if (!selfScan && sha1(emailAddress.email) === req.body.emailHash) {
selfScan = true;
break;
selfScan = true
break
}
}
}
}
const url = new URL(req.url, req.app.locals.SERVER_URL);
const url = new URL(req.url, req.app.locals.SERVER_URL)
const thisBreach = (breach) => {
return (element) => element.Name.toLowerCase() === breach.toLowerCase();
};
// Checks for a signedInUser arriving from doorhanger.
if (signedInUser && url.searchParams.has("utm_source") && url.searchParams.get("utm_source") === "firefox") {
doorhangerScan = true, selfScan = true;
specificBreach = allBreaches.find(thisBreach(req.query.breach));
return (element) => element.Name.toLowerCase() === breach.toLowerCase()
}
fullReport = url.pathname === "/full_report";
// Checks for a signedInUser arriving from doorhanger.
if (signedInUser && url.searchParams.has('utm_source') && url.searchParams.get('utm_source') === 'firefox') {
doorhangerScan = true, selfScan = true
specificBreach = allBreaches.find(thisBreach(req.query.breach))
}
userDash = url.pathname === "/user_dashboard";
fullReport = url.pathname === '/full_report'
userDash = url.pathname === '/user_dashboard'
if (selfScan) {
scannedEmail = sha1(signedInUser.primary_email);
scannedEmail = sha1(signedInUser.primary_email)
}
if (scannedEmail) {
// Gets sensitive breaches only if selfScan === true
foundBreaches = await HIBP.getBreachesForEmail(scannedEmail, allBreaches, selfScan);
foundBreaches = await HIBP.getBreachesForEmail(scannedEmail, allBreaches, selfScan)
}
// Checks if scan originated from a breach detail/"featured breach" page.
if (req.body && req.body.featuredBreach) {
specificBreach = allBreaches.find(thisBreach(req.body.featuredBreach));
specificBreach = allBreaches.find(thisBreach(req.body.featuredBreach))
}
if (doorhangerScan || specificBreach) {
const specificBreachIndex = foundBreaches.findIndex(breach => breach.Name === specificBreach.Name);
const specificBreachIndex = foundBreaches.findIndex(breach => breach.Name === specificBreach.Name)
// Checks foundBreaches for specificBreach and if found,
// brings specificBreach to front of foundBreaches list.
if (specificBreachIndex !== -1) {
userCompromised = true;
foundBreaches.splice(specificBreachIndex, 1);
foundBreaches.unshift(specificBreach);
userCompromised = true
foundBreaches.splice(specificBreachIndex, 1)
foundBreaches.unshift(specificBreach)
}
}
@ -100,55 +99,54 @@ const scanResult = async(req, selfScan=false) => {
fullReport,
userDash,
scannedEmailId,
experimentFlags,
};
};
experimentFlags
}
}
function resultsSummary(verifiedEmails) {
function resultsSummary (verifiedEmails) {
const breachStats = {
monitoredEmails: {
count: 0,
count: 0
},
numBreaches: {
count: 0,
numResolved: 0,
numResolved: 0
},
passwords: {
count: 0,
numResolved: 0,
},
};
let foundBreaches = [];
numResolved: 0
}
}
let foundBreaches = []
// combine the breaches for each account, breach duplicates are ok
// since the user may have multiple accounts with different emails
verifiedEmails.forEach(email => {
email.breaches.forEach(breach => {
if (breach.IsResolved) {
breachStats.numBreaches.numResolved++;
breachStats.numBreaches.numResolved++
}
const dataClasses = breach.DataClasses;
if (dataClasses.includes("passwords")) {
breachStats.passwords.count++;
const dataClasses = breach.DataClasses
if (dataClasses.includes('passwords')) {
breachStats.passwords.count++
if (breach.IsResolved) {
breachStats.passwords.numResolved++;
breachStats.passwords.numResolved++
}
}
});
foundBreaches = [...foundBreaches, ...email.breaches];
});
})
foundBreaches = [...foundBreaches, ...email.breaches]
})
// total number of verified emails being monitored
breachStats.monitoredEmails.count = verifiedEmails.length;
breachStats.monitoredEmails.count = verifiedEmails.length
// total number of breaches across all emails
breachStats.numBreaches.count = foundBreaches.length;
return breachStats;
breachStats.numBreaches.count = foundBreaches.length
return breachStats
}
module.exports = {
scanResult,
resultsSummary,
};
resultsSummary
}

Просмотреть файл

@ -1,51 +1,49 @@
"use strict";
'use strict'
const HIBP = require("../hibp");
const HIBP = require('../hibp')
// https://stackoverflow.com/a/8528531
function dhm(t){
const cd = 24 * 60 * 60 * 1000,
ch = 60 * 60 * 1000,
pad = (n) => { return n < 10 ? "0" + n : n; };
let d = Math.floor(t / cd),
h = Math.floor( (t - d * cd) / ch),
m = Math.round( (t - d * cd - h * ch) / 60000);
if( m === 60 ){
h++;
m = 0;
function dhm (t) {
const cd = 24 * 60 * 60 * 1000
const ch = 60 * 60 * 1000
const pad = (n) => { return n < 10 ? '0' + n : n }
let d = Math.floor(t / cd)
let h = Math.floor((t - d * cd) / ch)
let m = Math.round((t - d * cd - h * ch) / 60000)
if (m === 60) {
h++
m = 0
}
if( h === 24 ){
d++;
h = 0;
if (h === 24) {
d++
h = 0
}
return [d, pad(h), pad(m)].join(":");
return [d, pad(h), pad(m)].join(':')
}
(async () => {
const breaches = await HIBP.req("/breaches");
const breaches = await HIBP.req('/breaches')
let oldestBreachDate = new Date();
let oldestBreach = "";
let fastestResponseTime = Math.abs(new Date() - new Date(0));
let fastestResponseBreach = "";
let oldestBreachDate = new Date()
let oldestBreach = ''
let fastestResponseTime = Math.abs(new Date() - new Date(0))
let fastestResponseBreach = ''
for (const breach of breaches.body) {
console.log("checking breach: ", breach.Name);
const breachDate = new Date(breach.BreachDate);
if (breachDate < oldestBreachDate){
oldestBreachDate = breachDate;
oldestBreach = breach.Name;
console.log('checking breach: ', breach.Name)
const breachDate = new Date(breach.BreachDate)
if (breachDate < oldestBreachDate) {
oldestBreachDate = breachDate
oldestBreach = breach.Name
}
const responseTime = Math.abs(breachDate - new Date(breach.AddedDate));
const responseTime = Math.abs(breachDate - new Date(breach.AddedDate))
if (responseTime < fastestResponseTime) {
fastestResponseTime = responseTime;
fastestResponseBreach = breach.Name;
fastestResponseTime = responseTime
fastestResponseBreach = breach.Name
}
}
console.log("===========================");
console.log("oldest breach: ", oldestBreach, " on date: ", oldestBreachDate);
console.log("fastest breach response time (dd:hh:mm): ", dhm(Math.abs(fastestResponseTime)), " for breach: ", fastestResponseBreach);
})();
console.log('===========================')
console.log('oldest breach: ', oldestBreach, ' on date: ', oldestBreachDate)
console.log('fastest breach response time (dd:hh:mm): ', dhm(Math.abs(fastestResponseTime)), ' for breach: ', fastestResponseBreach)
})()

Просмотреть файл

@ -1,79 +1,77 @@
"use strict";
'use strict'
// TODO: Confirm db row has index
const Knex = require("knex");
const knexConfig = require("../db/knexfile");
const knex = Knex(knexConfig);
const Knex = require('knex')
const knexConfig = require('../db/knexfile')
const knex = Knex(knexConfig)
const HIBP = require("../hibp");
const HIBP = require('../hibp')
async function checkIfBreachesExist(sha1, breaches) {
const breachResults = await HIBP.getBreachesForEmail(sha1, breaches, true);
async function checkIfBreachesExist (sha1, breaches) {
const breachResults = await HIBP.getBreachesForEmail(sha1, breaches, true)
if (breachResults.length >= 1) {
return true;
return true
}
return false;
return false
}
function getArgsValue(argument) {
const cliArguments = process.argv;
function getArgsValue (argument) {
const cliArguments = process.argv
if (cliArguments.indexOf(argument) < 0) {
throw new Error(`You are missing the argument: ${argument}`);
throw new Error(`You are missing the argument: ${argument}`)
}
const arguemntIndex = cliArguments.indexOf(argument);
const value = cliArguments[(arguemntIndex + 1)];
const arguemntIndex = cliArguments.indexOf(argument)
const value = cliArguments[(arguemntIndex + 1)]
if (!value ) {
throw new Error(`No value set for ${argument}.`);
if (!value) {
throw new Error(`No value set for ${argument}.`)
}
const valueNumber = parseInt(value);
const valueNumber = parseInt(value)
if (Number.isNaN(valueNumber)) {
throw new Error(`The value for ${argument} is not an interger.`);
throw new Error(`The value for ${argument} is not an interger.`)
}
return valueNumber;
return valueNumber
}
(async () => {
console.log("Script starting");
console.log('Script starting')
const allHibpBreachesResp = await HIBP.req("/breaches");
const allHibpBreaches = allHibpBreachesResp.body;
const allHibpBreachesResp = await HIBP.req('/breaches')
const allHibpBreaches = allHibpBreachesResp.body
const limitQuery = getArgsValue("--limit");
const cohortSize = getArgsValue("--cohort-size");
const limitQuery = getArgsValue('--limit')
const cohortSize = getArgsValue('--cohort-size')
console.log(`The limit of this query is ${limitQuery}`);
console.log(`The target cohort size of this query is ${cohortSize}`);
console.log(`The limit of this query is ${limitQuery}`)
console.log(`The target cohort size of this query is ${cohortSize}`)
// "SELECT primary_email, primary_sha1 FROM subscribers WHERE signup_language LIKE 'en%' AND breaches_resolved IS NULL ORDER BY random();"
const results = await knex("subscribers").where("signup_language", "like", "en%").andWhere({breaches_resolved: null}).orderByRaw("RANDOM()").limit(limitQuery).select("primary_email", "primary_sha1");
const results = await knex('subscribers').where('signup_language', 'like', 'en%').andWhere({ breaches_resolved: null }).orderByRaw('RANDOM()').limit(limitQuery).select('primary_email', 'primary_sha1')
const cohort = [];
const cohort = []
for (const record of results) {
if (cohort.length > cohortSize) {
// Cohort size reached
break;
break
}
const sha1 = record.primary_sha1;
const isValidCohortMember = await checkIfBreachesExist(sha1, allHibpBreaches);
if (isValidCohortMember) { cohort.push(record.primary_email); }
const sha1 = record.primary_sha1
const isValidCohortMember = await checkIfBreachesExist(sha1, allHibpBreaches)
if (isValidCohortMember) { cohort.push(record.primary_email) }
}
console.log("Script completed! See final output:");
console.log(cohort.toString());
console.log('Script completed! See final output:')
console.log(cohort.toString())
process.exit();
})();
process.exit()
})()

Просмотреть файл

@ -1,9 +1,8 @@
"use strict";
const DB = require("../db/DB");
'use strict'
const DB = require('../db/DB');
(async () => {
await DB.deleteUnverifiedSubscribers();
process.exit();
})();
await DB.deleteUnverifiedSubscribers()
process.exit()
})()

Просмотреть файл

@ -1,32 +1,32 @@
"use strict";
'use strict'
const readline = require("readline");
const readline = require('readline')
const DB = require("../db/DB");
const DB = require('../db/DB')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
output: process.stdout
});
(async () => {
rl.question("What FXA primary email address? ", first_answer => {
rl.question("Please re-type the email address to confirm. ", async (second_answer) => {
rl.question('What FXA primary email address? ', first_answer => {
rl.question('Please re-type the email address to confirm. ', async (second_answer) => {
if (first_answer !== second_answer) {
console.error("Email addresses do not match.");
process.exit(1);
console.error('Email addresses do not match.')
process.exit(1)
}
const subscriber = await DB.getSubscriberByEmail(second_answer);
const subscriber = await DB.getSubscriberByEmail(second_answer)
if (!subscriber) {
console.error("Could not find subscriber.");
process.exit(1);
console.error('Could not find subscriber.')
process.exit(1)
}
await DB.deleteEmailAddressesByUid(subscriber.id);
await DB.deleteSubscriberByFxAUID(subscriber.fxa_uid);
console.log("Deleted email_addresses and subscribers records.");
process.exit();
});
});
})();
await DB.deleteEmailAddressesByUid(subscriber.id)
await DB.deleteSubscriberByFxAUID(subscriber.fxa_uid)
console.log('Deleted email_addresses and subscribers records.')
process.exit()
})
})
})()

Просмотреть файл

@ -1,50 +1,47 @@
"use strict";
'use strict'
const Knex = require('knex')
const knexConfig = require('../db/knexfile')
const knex = Knex(knexConfig)
const Knex = require("knex");
const knexConfig = require("../db/knexfile");
const knex = Knex(knexConfig);
const HIBP = require('../hibp')
const getSha1 = require('../sha1-utils')
const HIBP = require("../hibp");
const getSha1 = require("../sha1-utils");
async function subscribeLowercaseHashToHIBP(emailAddress) {
const lowerCasedEmail = emailAddress.toLowerCase();
const lowerCasedSha1 = getSha1(lowerCasedEmail);
await HIBP.subscribeHash(lowerCasedSha1);
return lowerCasedSha1;
async function subscribeLowercaseHashToHIBP (emailAddress) {
const lowerCasedEmail = emailAddress.toLowerCase()
const lowerCasedSha1 = getSha1(lowerCasedEmail)
await HIBP.subscribeHash(lowerCasedSha1)
return lowerCasedSha1
}
(async () => {
const subRecordsWithUpperChars = await knex.select("id", "primary_email").from("subscribers")
.whereRaw("primary_email != lower(primary_email)");
const subsWithUpperCount = subRecordsWithUpperChars.length;
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`);
const subRecordsWithUpperChars = await knex.select('id', 'primary_email').from('subscribers')
.whereRaw('primary_email != lower(primary_email)')
const subsWithUpperCount = subRecordsWithUpperChars.length
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`)
for (const subRecord of subRecordsWithUpperChars) {
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(subRecord.primary_email);
await knex("subscribers")
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(subRecord.primary_email)
await knex('subscribers')
.update({
primary_sha1: lowerCasedSha1,
primary_sha1: lowerCasedSha1
})
.where("id", subRecord.id);
console.log(`fixed subscribers record ID: ${subRecord.id}`);
.where('id', subRecord.id)
console.log(`fixed subscribers record ID: ${subRecord.id}`)
}
const emailRecordsWithUpperChars = await knex.select("id", "email").from("email_addresses")
.whereRaw("email != lower(email)");
const emailsWithUpperCount = emailRecordsWithUpperChars.length;
console.log(`found ${emailsWithUpperCount} email_addresses records with email != lower(email)`);
const emailRecordsWithUpperChars = await knex.select('id', 'email').from('email_addresses')
.whereRaw('email != lower(email)')
const emailsWithUpperCount = emailRecordsWithUpperChars.length
console.log(`found ${emailsWithUpperCount} email_addresses records with email != lower(email)`)
for (const emailRecord of emailRecordsWithUpperChars) {
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(emailRecord.email);
await knex("email_addresses")
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(emailRecord.email)
await knex('email_addresses')
.update({
sha1: lowerCasedSha1,
sha1: lowerCasedSha1
})
.where("id", emailRecord.id);
console.log(`fixed email_addresses record ID: ${emailRecord.id}`);
.where('id', emailRecord.id)
console.log(`fixed email_addresses record ID: ${emailRecord.id}`)
}
console.log("done.");
process.exit();
})();
console.log('done.')
process.exit()
})()

Просмотреть файл

@ -1,15 +1,15 @@
"use strict";
'use strict'
const getSha1 = require("../sha1-utils");
const stdin = process.openStdin();
const getSha1 = require('../sha1-utils')
const stdin = process.openStdin()
const PROMPT = "\nEnter an email address to get the SHA1 hash as it would appear in a HIBP hashset file:";
const PROMPT = '\nEnter an email address to get the SHA1 hash as it would appear in a HIBP hashset file:'
console.log(PROMPT);
console.log(PROMPT)
stdin.addListener("data", data => {
const trimmedString = data.toString().trim();
const sha1 = getSha1(trimmedString);
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${sha1}`);
console.log(PROMPT);
});
stdin.addListener('data', data => {
const trimmedString = data.toString().trim()
const sha1 = getSha1(trimmedString)
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${sha1}`)
console.log(PROMPT)
})

Просмотреть файл

@ -1,52 +1,51 @@
"use strict";
'use strict'
const readline = require("readline");
const readline = require('readline')
const AppConstants = require("../app-constants");
const EmailUtils = require("../email-utils");
const hibp = require("../controllers/hibp");
const { LocaleUtils } = require("../locale-utils");
const sha1 = require("../sha1-utils");
const AppConstants = require('../app-constants')
const EmailUtils = require('../email-utils')
const hibp = require('../controllers/hibp')
const { LocaleUtils } = require('../locale-utils')
const sha1 = require('../sha1-utils')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const app = { locals: { breaches: [], AVAILABLE_LANGUAGES: ["en"] } };
output: process.stdout
})
const app = { locals: { breaches: [], AVAILABLE_LANGUAGES: ['en'] } }
LocaleUtils.init();
LocaleUtils.init()
LocaleUtils.loadLanguagesIntoApp(app);
(async () => {
await EmailUtils.init();
let emailAddress, breachName;
await EmailUtils.init()
let emailAddress, breachName
const resp = {
status: () => {},
json: (arg) => {
console.log(JSON.stringify(arg));
},
};
rl.question("What email address? ", (answer) => {
emailAddress = answer;
const hash = sha1(emailAddress);
const hashPrefix = hash.slice(0, 6).toUpperCase();
const hashSuffix = hash.slice(6).toUpperCase();
rl.question("What breach name? ", async (answer) => {
breachName = answer;
console.log(JSON.stringify(arg))
}
}
rl.question('What email address? ', (answer) => {
emailAddress = answer
const hash = sha1(emailAddress)
const hashPrefix = hash.slice(0, 6).toUpperCase()
const hashSuffix = hash.slice(6).toUpperCase()
rl.question('What breach name? ', async (answer) => {
breachName = answer
const req = {
token: AppConstants.HIBP_NOTIFY_TOKEN,
app: app,
body: { hashPrefix, hashSuffixes: [hashSuffix], breachName },
};
app,
body: { hashPrefix, hashSuffixes: [hashSuffix], breachName }
}
await hibp.notify(req, resp);
await hibp.notify(req, resp)
console.log(JSON.stringify(resp));
console.log(JSON.stringify(resp))
rl.close();
rl.close()
process.exit();
});
});
})();
process.exit()
})
})
})()

Просмотреть файл

@ -1,70 +1,68 @@
"use strict";
'use strict'
/* eslint-disable no-process-env */
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
const AppConstants = require("../app-constants");
const DB = require("../db/DB");
const EmailHelpers = require("../template-helpers/emails.js");
const EmailUtils = require("../email-utils");
const { LocaleUtils } = require ("../locale-utils");
const AppConstants = require('../app-constants')
const DB = require('../db/DB')
const EmailHelpers = require('../template-helpers/emails.js')
const EmailUtils = require('../email-utils')
const { LocaleUtils } = require('../locale-utils')
const PAGE_SIZE = process.env.PAGE_SIZE;
const START_PAGE = process.env.START_PAGE;
const PAGE_SIZE = process.env.PAGE_SIZE
const START_PAGE = process.env.START_PAGE
if (!START_PAGE) {
console.error("You must provide a START_PAGE environment variable.");
process.exit();
console.error('You must provide a START_PAGE environment variable.')
process.exit()
}
(async (req) => {
const localeUtils = LocaleUtils.init();
EmailUtils.init();
const notifiedSubscribers = [];
const utmID = "pre-fxa";
const subscribersResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage: START_PAGE, isLengthAware: true });
const numPagesToProcess = subscribersResult.pagination.lastPage - START_PAGE;
console.log(`Found ${subscribersResult.pagination.total} subscriber records with empty fxa_uid.`);
console.log(`Will process ${numPagesToProcess} pages of size ${PAGE_SIZE}, starting with page ${START_PAGE} and ending with page ${subscribersResult.pagination.lastPage}.`);
const lastPage = subscribersResult.pagination.lastPage;
const localeUtils = LocaleUtils.init()
EmailUtils.init()
const notifiedSubscribers = []
const utmID = 'pre-fxa'
const subscribersResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage: START_PAGE, isLengthAware: true })
const numPagesToProcess = subscribersResult.pagination.lastPage - START_PAGE
console.log(`Found ${subscribersResult.pagination.total} subscriber records with empty fxa_uid.`)
console.log(`Will process ${numPagesToProcess} pages of size ${PAGE_SIZE}, starting with page ${START_PAGE} and ending with page ${subscribersResult.pagination.lastPage}.`)
const lastPage = subscribersResult.pagination.lastPage
for (let currentPage = START_PAGE; currentPage <= lastPage; currentPage++) {
console.log(`Processing page ${currentPage} of ${lastPage}.`);
const subscribersPageResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage });
console.log(`Processing page ${currentPage} of ${lastPage}.`)
const subscribersPageResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage })
for (const subscriber of subscribersPageResult.data) {
const signupLanguage = subscriber.signup_language;
const subscriberEmail = subscriber.primary_email;
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : "";
const signupLanguage = subscriber.signup_language
const subscriberEmail = subscriber.primary_email
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : ''
const supportedLocales = negotiateLanguages(
requestedLanguage,
localeUtils.availableLanguages,
{defaultLocale: "en"}
);
{ defaultLocale: 'en' }
)
if (!notifiedSubscribers.includes(subscriberEmail)) {
const sendInfo = await EmailUtils.sendEmail(
subscriberEmail,
LocaleUtils.fluentFormat(supportedLocales, "pre-fxa-subject"), // email subject
"default_email", // email template
LocaleUtils.fluentFormat(supportedLocales, 'pre-fxa-subject'), // email subject
'default_email', // email template
{
supportedLocales,
SERVER_URL: AppConstants.SERVER_URL,
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(subscriber, utmID), // need to test the flow for legacy users who want to unsubscribe
ctaHref: EmailHelpers.getPreFxaUtmParams(AppConstants.SERVER_URL, "create-account-button", subscriberEmail),
whichPartial: "email_partials/pre-fxa",
ctaHref: EmailHelpers.getPreFxaUtmParams(AppConstants.SERVER_URL, 'create-account-button', subscriberEmail),
whichPartial: 'email_partials/pre-fxa',
preFxaEmail: true,
email: subscriberEmail,
email: subscriberEmail
}
);
notifiedSubscribers.push(subscriberEmail);
console.log(`Sent email to ${subscriberEmail}, info: ${JSON.stringify(sendInfo)}`);
)
notifiedSubscribers.push(subscriberEmail)
console.log(`Sent email to ${subscriberEmail}, info: ${JSON.stringify(sendInfo)}`)
}
}
}
console.log(`Notified subscribers: ${JSON.stringify(notifiedSubscribers)}`);
process.exit();
})();
console.log(`Notified subscribers: ${JSON.stringify(notifiedSubscribers)}`)
process.exit()
})()

Просмотреть файл

@ -1,68 +1,64 @@
"use strict";
'use strict'
const Knex = require('knex')
const knexConfig = require('../db/knexfile')
const knex = Knex(knexConfig)
const Knex = require("knex");
const knexConfig = require("../db/knexfile");
const knex = Knex(knexConfig);
const HIBP = require('../hibp')
const getSha1 = require('../sha1-utils')
const HIBP = require("../hibp");
const getSha1 = require("../sha1-utils");
async function subscribeLowercaseHashToHIBP(emailAddress) {
const lowerCasedEmail = emailAddress.toLowerCase();
const lowerCasedSha1 = getSha1(lowerCasedEmail);
await HIBP.subscribeHash(lowerCasedSha1);
return lowerCasedSha1;
async function subscribeLowercaseHashToHIBP (emailAddress) {
const lowerCasedEmail = emailAddress.toLowerCase()
const lowerCasedSha1 = getSha1(lowerCasedEmail)
await HIBP.subscribeHash(lowerCasedSha1)
return lowerCasedSha1
}
(async () => {
const chunkSize = process.argv[2];
console.log(`subscribing lower-cased hashes in ${chunkSize}-sized chunks`);
const chunkSize = process.argv[2]
console.log(`subscribing lower-cased hashes in ${chunkSize}-sized chunks`)
const subRecordsThatNeedFixing = await knex("subscribers").count().whereRaw("primary_email != lower(primary_email)");
const subsWithUpperCount = subRecordsThatNeedFixing[0].count;
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`);
const subRecordsThatNeedFixing = await knex('subscribers').count().whereRaw('primary_email != lower(primary_email)')
const subsWithUpperCount = subRecordsThatNeedFixing[0].count
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`)
let subRecordsFixed = 0;
let subPrevMaxId = 0;
let subRecordsFixed = 0
let subPrevMaxId = 0
while (subRecordsFixed < subsWithUpperCount) {
console.log(`working on chunk where id > ${subPrevMaxId} ...`);
const subRecordsWithUpperCharsChunk = await knex.select("id", "primary_email").from("subscribers")
.where("id", ">", subPrevMaxId)
.whereRaw("primary_email != lower(primary_email)")
.orderBy("id", "asc")
.limit(chunkSize);
console.log(`working on chunk where id > ${subPrevMaxId} ...`)
const subRecordsWithUpperCharsChunk = await knex.select('id', 'primary_email').from('subscribers')
.where('id', '>', subPrevMaxId)
.whereRaw('primary_email != lower(primary_email)')
.orderBy('id', 'asc')
.limit(chunkSize)
for (const subRecord of subRecordsWithUpperCharsChunk) {
await subscribeLowercaseHashToHIBP(subRecord.primary_email);
subPrevMaxId = subRecord.id;
subRecordsFixed++;
console.log(`subscribed lower-case address hash for subscribers record ID: ${subRecord.id}`);
await subscribeLowercaseHashToHIBP(subRecord.primary_email)
subPrevMaxId = subRecord.id
subRecordsFixed++
console.log(`subscribed lower-case address hash for subscribers record ID: ${subRecord.id}`)
}
}
const emailRecordsThatNeedFixing = await knex("email_addresses").count().whereRaw("email != lower(email)");
const emailWithUpperCount = emailRecordsThatNeedFixing[0].count;
console.log(`found ${emailWithUpperCount} email_address records with email != lower(email). fixing ...`);
const emailRecordsThatNeedFixing = await knex('email_addresses').count().whereRaw('email != lower(email)')
const emailWithUpperCount = emailRecordsThatNeedFixing[0].count
console.log(`found ${emailWithUpperCount} email_address records with email != lower(email). fixing ...`)
let emailRecordsFixed = 0;
let emailPrevMaxId = 0;
let emailRecordsFixed = 0
let emailPrevMaxId = 0
while (emailRecordsFixed < emailWithUpperCount) {
console.log(`working on chunk where id > ${emailPrevMaxId} ...`);
const emailRecordsWithUpperChars = await knex.select("id", "email").from("email_addresses")
.where("id", ">", emailPrevMaxId)
.whereRaw("email != lower(email)")
.orderBy("id", "asc")
.limit(chunkSize);
console.log(`working on chunk where id > ${emailPrevMaxId} ...`)
const emailRecordsWithUpperChars = await knex.select('id', 'email').from('email_addresses')
.where('id', '>', emailPrevMaxId)
.whereRaw('email != lower(email)')
.orderBy('id', 'asc')
.limit(chunkSize)
for (const emailRecord of emailRecordsWithUpperChars) {
await subscribeLowercaseHashToHIBP(emailRecord.email);
emailPrevMaxId = emailRecord.id;
emailRecordsFixed++;
console.log(`fixed email_addresses record ID: ${emailRecord.id}`);
await subscribeLowercaseHashToHIBP(emailRecord.email)
emailPrevMaxId = emailRecord.id
emailRecordsFixed++
console.log(`fixed email_addresses record ID: ${emailRecord.id}`)
}
}
console.log("done.");
process.exit();
})();
console.log('done.')
process.exit()
})()

Просмотреть файл

@ -1,33 +1,31 @@
"use strict";
const AppConstants = require("../app-constants");
const HIBP = require("../hibp");
const RemoteSettings = require("../lib/remote-settings");
'use strict'
const AppConstants = require('../app-constants')
const HIBP = require('../hibp')
const RemoteSettings = require('../lib/remote-settings')
if (
!AppConstants.FX_REMOTE_SETTINGS_WRITER_USER ||
!AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS ||
!AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER
) {
console.error("updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.");
process.exit(1);
console.error('updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.')
process.exit(1)
}
(async () => {
const allHibpBreaches = await HIBP.req("/breaches");
const verifiedSiteBreaches = HIBP.filterBreaches(allHibpBreaches.body);
const verifiedSiteBreachesWithPWs = verifiedSiteBreaches.filter(breach => breach.DataClasses.includes("Passwords"));
const allHibpBreaches = await HIBP.req('/breaches')
const verifiedSiteBreaches = HIBP.filterBreaches(allHibpBreaches.body)
const verifiedSiteBreachesWithPWs = verifiedSiteBreaches.filter(breach => breach.DataClasses.includes('Passwords'))
const newBreaches = await RemoteSettings.whichBreachesAreNotInRemoteSettingsYet(verifiedSiteBreachesWithPWs);
const newBreaches = await RemoteSettings.whichBreachesAreNotInRemoteSettingsYet(verifiedSiteBreachesWithPWs)
if (newBreaches.length <= 0) {
console.log("No new breaches detected.");
process.exit(0);
console.log('No new breaches detected.')
process.exit(0)
}
console.log(`${newBreaches.length} new breach(es) found.`);
console.log(`${newBreaches.length} new breach(es) found.`)
for (const breach of newBreaches) {
const data = {
@ -36,20 +34,19 @@ if (
BreachDate: breach.BreachDate,
PwnCount: breach.PwnCount,
AddedDate: breach.AddedDate,
DataClasses: breach.DataClasses,
};
DataClasses: breach.DataClasses
}
console.log("New breach detected: \n", data);
console.log('New breach detected: \n', data)
try {
await RemoteSettings.postNewBreachToBreachesCollection(data);
await RemoteSettings.postNewBreachToBreachesCollection(data)
} catch (e) {
console.error(e);
process.exit(1);
console.error(e)
process.exit(1)
}
}
console.log("Requesting review on breaches collection");
await RemoteSettings.requestReviewOnBreachesCollection();
})();
console.log('Requesting review on breaches collection')
await RemoteSettings.requestReviewOnBreachesCollection()
})()

285
server.js
Просмотреть файл

@ -1,149 +1,148 @@
"use strict";
'use strict'
// initialize Sentry ASAP to capture fatal startup errors
const Sentry = require("@sentry/node");
const AppConstants = require("./app-constants");
const Sentry = require('@sentry/node')
const AppConstants = require('./app-constants')
Sentry.init({
dsn: AppConstants.SENTRY_DSN,
environment: AppConstants.NODE_ENV,
beforeSend(event, hint) {
if (!hint.originalException.locales || hint.originalException.locales[0] === "en") return event; // return if no localization or localization is in english
beforeSend (event, hint) {
if (!hint.originalException.locales || hint.originalException.locales[0] === 'en') return event // return if no localization or localization is in english
try {
if (hint.originalException.fluentID) {
event.exception.values[0].value = LocaleUtils.fluentFormat(["en"], hint.originalException.fluentID);
event.exception.values[0].value = LocaleUtils.fluentFormat(['en'], hint.originalException.fluentID)
}
} catch (e) {
return event;
return event
}
return event;
},
});
return event
}
})
const connectRedis = require("connect-redis");
const express = require("express");
const exphbs = require("express-handlebars");
const helmet = require("helmet");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const { URL } = require("url");
const connectRedis = require('connect-redis')
const express = require('express')
const exphbs = require('express-handlebars')
const helmet = require('helmet')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const { URL } = require('url')
const EmailUtils = require("./email-utils");
const HBSHelpers = require("./template-helpers/");
const HIBP = require("./hibp");
const IpLocationService = require("./ip-location-service");
const EmailUtils = require('./email-utils')
const HBSHelpers = require('./template-helpers/')
const HIBP = require('./hibp')
const IpLocationService = require('./ip-location-service')
const {
addRequestToResponse, pickLanguage, logErrors, localizeErrorMessages,
clientErrorHandler, errorHandler, recordVisitFromEmail,
} = require("./middleware");
const { LocaleUtils } = require("./locale-utils");
const mozlog = require("./log");
clientErrorHandler, errorHandler, recordVisitFromEmail
} = require('./middleware')
const { LocaleUtils } = require('./locale-utils')
const mozlog = require('./log')
const DockerflowRoutes = require("./routes/dockerflow");
const HibpRoutes = require("./routes/hibp");
const HomeRoutes = require("./routes/home");
const ScanRoutes = require("./routes/scan");
const SesRoutes = require("./routes/ses");
const OAuthRoutes = require("./routes/oauth");
const UserRoutes = require("./routes/user");
const EmailL10nRoutes= require("./routes/email-l10n");
const BreachRoutes= require("./routes/breach-details");
const DockerflowRoutes = require('./routes/dockerflow')
const HibpRoutes = require('./routes/hibp')
const HomeRoutes = require('./routes/home')
const ScanRoutes = require('./routes/scan')
const SesRoutes = require('./routes/ses')
const OAuthRoutes = require('./routes/oauth')
const UserRoutes = require('./routes/user')
const EmailL10nRoutes = require('./routes/email-l10n')
const BreachRoutes = require('./routes/breach-details')
const log = mozlog("server");
const log = mozlog('server')
function getRedisStore() {
const redisStoreConstructor = connectRedis(session);
if (["", "redis-mock"].includes(AppConstants.REDIS_URL)) {
function getRedisStore () {
const redisStoreConstructor = connectRedis(session)
if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
// eslint-disable-next-line node/no-unpublished-require
const redis = require("redis-mock");
return new redisStoreConstructor({ client: redis.createClient() });
const redis = require('redis-mock')
return new redisStoreConstructor({ client: redis.createClient() })
}
const redis = require("redis");
return new redisStoreConstructor({ client: redis.createClient({url: AppConstants.REDIS_URL }) });
const redis = require('redis')
return new redisStoreConstructor({ client: redis.createClient({ url: AppConstants.REDIS_URL }) })
}
const app = express();
const app = express()
function devOrHeroku() {
return ["dev", "heroku"].includes(AppConstants.NODE_ENV);
function devOrHeroku () {
return ['dev', 'heroku'].includes(AppConstants.NODE_ENV)
}
if (app.get("env") !== "dev") {
app.enable("trust proxy");
app.use( (req, res, next) => {
if (app.get('env') !== 'dev') {
app.enable('trust proxy')
app.use((req, res, next) => {
if (req.secure) {
next();
next()
} else {
res.redirect("https://" + req.headers.host + req.url);
res.redirect('https://' + req.headers.host + req.url)
}
});
})
}
try {
LocaleUtils.init();
LocaleUtils.loadLanguagesIntoApp(app);
LocaleUtils.init()
LocaleUtils.loadLanguagesIntoApp(app)
} catch (error) {
log.error("try-load-languages-error", { error: error });
log.error('try-load-languages-error', { error })
}
(async () => {
try {
await HIBP.loadBreachesIntoApp(app);
await HIBP.loadBreachesIntoApp(app)
} catch (error) {
log.error("try-load-breaches-error", { error: error });
log.error('try-load-breaches-error', { error })
}
})();
(async () => {
// open location database once at server start. Service includes 24hr check to reload fresh database.
await IpLocationService.openLocationDb().catch(e => console.warn(e));
})();
await IpLocationService.openLocationDb().catch(e => console.warn(e))
})()
// Use helmet to set security headers
// only enable HSTS on heroku; Ops handles it in stage & prod configs
if (AppConstants.NODE_ENV === "heroku") {
app.use(helmet.hsts({ maxAge: 60 * 60 * 24 * 365 * 2 })); // 2 years
if (AppConstants.NODE_ENV === 'heroku') {
app.use(helmet.hsts({ maxAge: 60 * 60 * 24 * 365 * 2 })) // 2 years
}
const SCRIPT_SOURCES = ["'self'", "https://www.google-analytics.com/analytics.js"];
const STYLE_SOURCES = ["'self'", "https://code.cdn.mozilla.net/fonts/"];
const FRAME_ANCESTORS = ["'none'"];
const SCRIPT_SOURCES = ["'self'", 'https://www.google-analytics.com/analytics.js']
const STYLE_SOURCES = ["'self'", 'https://code.cdn.mozilla.net/fonts/']
const FRAME_ANCESTORS = ["'none'"]
app.locals.ENABLE_PONTOON_JS = false;
app.locals.ENABLE_PONTOON_JS = false
// Allow pontoon.mozilla.org on heroku for in-page localization
const PONTOON_DOMAIN = "https://pontoon.mozilla.org";
if (AppConstants.NODE_ENV === "heroku") {
app.locals.ENABLE_PONTOON_JS = true;
SCRIPT_SOURCES.push(PONTOON_DOMAIN);
STYLE_SOURCES.push(PONTOON_DOMAIN);
FRAME_ANCESTORS[0] = PONTOON_DOMAIN; // other sources cannot be declared alongside 'none'
const PONTOON_DOMAIN = 'https://pontoon.mozilla.org'
if (AppConstants.NODE_ENV === 'heroku') {
app.locals.ENABLE_PONTOON_JS = true
SCRIPT_SOURCES.push(PONTOON_DOMAIN)
STYLE_SOURCES.push(PONTOON_DOMAIN)
FRAME_ANCESTORS[0] = PONTOON_DOMAIN // other sources cannot be declared alongside 'none'
}
const imgSrc = [
"'self'",
"https://www.google-analytics.com",
"https://firefoxusercontent.com",
"https://mozillausercontent.com/",
"https://monitor.cdn.mozilla.net/",
];
'https://www.google-analytics.com',
'https://firefoxusercontent.com',
'https://mozillausercontent.com/',
'https://monitor.cdn.mozilla.net/'
]
const connectSrc = [
"'self'",
"https://code.cdn.mozilla.net/fonts/",
"https://www.google-analytics.com",
"https://accounts.firefox.com",
"https://accounts.stage.mozaws.net/metrics-flow",
"https://am.i.mullvad.net/json",
];
'https://code.cdn.mozilla.net/fonts/',
'https://www.google-analytics.com',
'https://accounts.firefox.com',
'https://accounts.stage.mozaws.net/metrics-flow',
'https://am.i.mullvad.net/json'
]
if (AppConstants.FXA_ENABLED) {
const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin;
[imgSrc, connectSrc].forEach(arr => {
arr.push(fxaSrc);
});
arr.push(fxaSrc)
})
}
app.use(
@ -151,103 +150,103 @@ app.use(
directives: {
baseUri: ["'none'"],
defaultSrc: ["'self'"],
connectSrc: connectSrc,
connectSrc,
fontSrc: [
"'self'",
"https://fonts.gstatic.com/",
"https://code.cdn.mozilla.net/fonts/",
'https://fonts.gstatic.com/',
'https://code.cdn.mozilla.net/fonts/'
],
frameAncestors: FRAME_ANCESTORS,
mediaSrc: [
"'self'",
"https://monitor.cdn.mozilla.net/",
'https://monitor.cdn.mozilla.net/'
],
formAction: ["'self'"],
imgSrc: imgSrc,
imgSrc,
objectSrc: ["'none'"],
scriptSrc: SCRIPT_SOURCES,
styleSrc: STYLE_SOURCES,
reportUri: "/__cspreport__",
},
reportUri: '/__cspreport__'
}
})
);
app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));
)
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }))
// helmet no longer sets X-Content-Type-Options, so set it manually
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
next();
});
res.setHeader('X-Content-Type-Options', 'nosniff')
next()
})
app.use(express.static("public", {
setHeaders: res => res.set("Cache-Control",
"public, maxage=" + 10 * 60 * 1000 + ", s-maxage=" + 24 * 60 * 60 * 1000),
})); // 10-minute client-side caching; 24-hour server-side caching
app.use(express.static('public', {
setHeaders: res => res.set('Cache-Control',
'public, maxage=' + 10 * 60 * 1000 + ', s-maxage=' + 24 * 60 * 60 * 1000)
})) // 10-minute client-side caching; 24-hour server-side caching
app.use(cookieParser());
app.use(cookieParser())
const hbs = exphbs.create({
extname: ".hbs",
layoutsDir: __dirname + "/views/layouts",
defaultLayout: "default",
partialsDir: __dirname + "/views/partials",
helpers: HBSHelpers.helpers,
});
app.engine("hbs", hbs.engine);
app.set("view engine", "hbs");
extname: '.hbs',
layoutsDir: __dirname + '/views/layouts',
defaultLayout: 'default',
partialsDir: __dirname + '/views/partials',
helpers: HBSHelpers.helpers
})
app.engine('hbs', hbs.engine)
app.set('view engine', 'hbs')
// TODO: refactor all templates to use constants.VAR
// instead of assigning these 1-by-1 to app.locales
app.locals.constants = AppConstants;
app.locals.FXA_ENABLED = AppConstants.FXA_ENABLED;
app.locals.SERVER_URL = AppConstants.SERVER_URL;
app.locals.MAX_NUM_ADDRESSES = AppConstants.MAX_NUM_ADDRESSES;
app.locals.EXPERIMENT_ACTIVE = AppConstants.EXPERIMENT_ACTIVE;
app.locals.RECRUITMENT_BANNER_LINK = AppConstants.RECRUITMENT_BANNER_LINK;
app.locals.RECRUITMENT_BANNER_TEXT = AppConstants.RECRUITMENT_BANNER_TEXT;
app.locals.LOGOS_ORIGIN = AppConstants.LOGOS_ORIGIN;
app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname;
app.locals.constants = AppConstants
app.locals.FXA_ENABLED = AppConstants.FXA_ENABLED
app.locals.SERVER_URL = AppConstants.SERVER_URL
app.locals.MAX_NUM_ADDRESSES = AppConstants.MAX_NUM_ADDRESSES
app.locals.EXPERIMENT_ACTIVE = AppConstants.EXPERIMENT_ACTIVE
app.locals.RECRUITMENT_BANNER_LINK = AppConstants.RECRUITMENT_BANNER_LINK
app.locals.RECRUITMENT_BANNER_TEXT = AppConstants.RECRUITMENT_BANNER_TEXT
app.locals.LOGOS_ORIGIN = AppConstants.LOGOS_ORIGIN
app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48;
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
app.use(session({
cookie: {
httpOnly: true,
maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
rolling: true,
sameSite: "lax",
secure: AppConstants.NODE_ENV !== "dev",
sameSite: 'lax',
secure: AppConstants.NODE_ENV !== 'dev'
},
resave: false,
saveUninitialized: true,
secret: AppConstants.COOKIE_SECRET,
store: getRedisStore(),
}));
store: getRedisStore()
}))
app.use(pickLanguage);
app.use(addRequestToResponse);
app.use(recordVisitFromEmail);
app.use(pickLanguage)
app.use(addRequestToResponse)
app.use(recordVisitFromEmail)
app.use("/", DockerflowRoutes);
app.use("/hibp", HibpRoutes);
app.use('/', DockerflowRoutes)
app.use('/hibp', HibpRoutes)
if (AppConstants.FXA_ENABLED) {
app.use("/oauth", OAuthRoutes);
app.use('/oauth', OAuthRoutes)
}
app.use("/scan", ScanRoutes);
app.use("/ses", SesRoutes);
app.use("/user", UserRoutes);
(devOrHeroku ? app.use("/email-l10n", EmailL10nRoutes) : null);
app.use("/breach-details", BreachRoutes);
app.use("/", HomeRoutes);
app.use('/scan', ScanRoutes)
app.use('/ses', SesRoutes)
app.use('/user', UserRoutes);
(devOrHeroku ? app.use('/email-l10n', EmailL10nRoutes) : null)
app.use('/breach-details', BreachRoutes)
app.use('/', HomeRoutes)
app.use(logErrors);
app.use(localizeErrorMessages);
app.use(clientErrorHandler);
app.use(errorHandler);
app.use(logErrors)
app.use(localizeErrorMessages)
app.use(clientErrorHandler)
app.use(errorHandler)
EmailUtils.init().then(() => {
const listener = app.listen(AppConstants.PORT, () => {
log.info("Listening", { port: listener.address().port });
});
log.info('Listening', { port: listener.address().port })
})
}).catch(error => {
log.error("try-initialize-email-error", { error: error });
});
log.error('try-initialize-email-error', { error })
})

Просмотреть файл

@ -1,9 +1,9 @@
"use strict";
'use strict'
const crypto = require("crypto");
const crypto = require('crypto')
function getSha1(email) {
return crypto.createHash("sha1").update(email).digest("hex");
function getSha1 (email) {
return crypto.createHash('sha1').update(email).digest('hex')
}
module.exports = getSha1;
module.exports = getSha1

Просмотреть файл

@ -1,513 +1,514 @@
"use strict";
'use strict'
const { LocaleUtils } = require("./../locale-utils");
const { LocaleUtils } = require('./../locale-utils')
function getSecurityTipsIntro() {
function getSecurityTipsIntro () {
return [
"Data breaches are becoming more common. Finding out you were part of one usually includes a long list of compromised information, such as your password, username, and email address. What does that mean for your internet safety? What should you do? Learn how you can take control after a data breach and better protect your devices, online accounts, and personal data from cyber criminals.",
];
'Data breaches are becoming more common. Finding out you were part of one usually includes a long list of compromised information, such as your password, username, and email address. What does that mean for your internet safety? What should you do? Learn how you can take control after a data breach and better protect your devices, online accounts, and personal data from cyber criminals.'
]
}
const articleCopy = {
"how-hackers-work": {
'how-hackers-work': {
paragraphs: [
{
dropCap: "F",
leads : [
"orget about those hackers in movies trying to crack the code on someones computer to get their top-secret files. The hackers responsible for data breaches usually start by targeting companies, rather than specific individuals. They want to get data from as many people as possible so they can use, resell, or leverage it to make money. It all starts with getting your password.",
],
dropCap: 'F',
leads: [
'orget about those hackers in movies trying to crack the code on someones computer to get their top-secret files. The hackers responsible for data breaches usually start by targeting companies, rather than specific individuals. They want to get data from as many people as possible so they can use, resell, or leverage it to make money. It all starts with getting your password.'
]
},
{
subhead: "It's not personal. Not at first.",
paragraphs: [
"Hackers dont really care whose personal information and credentials they can get, as long as they can get a lot of it. Thats why cyber criminals often target massive companies with millions of users. These hackers look for a security weakness — the digital equivalent of leaving a door unlocked or window open. They only need to find one door or window to get inside. Then they steal or copy as much personal information as possible.",
"Once they get your data, cyber criminals can start their real work. We dont always know what they intend to do with the data, but usually they will try to find a way to profit from it. The effects on you might not be immediate. But they can be very serious.",
],
'Hackers dont really care whose personal information and credentials they can get, as long as they can get a lot of it. Thats why cyber criminals often target massive companies with millions of users. These hackers look for a security weakness — the digital equivalent of leaving a door unlocked or window open. They only need to find one door or window to get inside. Then they steal or copy as much personal information as possible.',
'Once they get your data, cyber criminals can start their real work. We dont always know what they intend to do with the data, but usually they will try to find a way to profit from it. The effects on you might not be immediate. But they can be very serious.'
]
},
{
subhead: "All types of data can be valuable.",
subhead: 'All types of data can be valuable.',
paragraphs: [
"Some data — like banking information, bank card numbers, government-issued ID numbers, and PIN numbers — is valuable because it can be used to steal the victims identity or withdraw money. Email addresses and passwords are also valuable because hackers can try them on other accounts. All sorts of data can be valuable in some way because it can be sold on the dark web for a profit or kept for some future use.",
],
},
'Some data — like banking information, bank card numbers, government-issued ID numbers, and PIN numbers — is valuable because it can be used to steal the victims identity or withdraw money. Email addresses and passwords are also valuable because hackers can try them on other accounts. All sorts of data can be valuable in some way because it can be sold on the dark web for a profit or kept for some future use.'
]
},
{
subhead: "Common passwords make a hackers work easy.",
subhead: 'Common passwords make a hackers work easy.',
paragraphs: [
"Hackers arent actually guessing peoples passwords. To crack into accounts, they use automated programs that enter hundreds of popular passwords in just a few seconds. Thats why its important to avoid using the same passwords that everyone else does.",
'Hackers arent actually guessing peoples passwords. To crack into accounts, they use automated programs that enter hundreds of popular passwords in just a few seconds. Thats why its important to avoid using the same passwords that everyone else does.'
],
list: [
"123456 and password are the most commonly used passwords. Dont use them.",
"Switching a letter for a symbol (p@ssw0rd!) is an obvious trick hackers know well.",
"Avoid favorite sports teams or pop culture references. Use something more obscure.",
"Dont use a single word like sunshine, monkey, or football. Using a phrase or sentence as your password is stronger.",
"Dont use common number patterns like 111111, abc123, or 654321.",
"Adding a number or piece of punctuation at the end doesnt make your password stronger.",
],
'123456 and password are the most commonly used passwords. Dont use them.',
'Switching a letter for a symbol (p@ssw0rd!) is an obvious trick hackers know well.',
'Avoid favorite sports teams or pop culture references. Use something more obscure.',
'Dont use a single word like sunshine, monkey, or football. Using a phrase or sentence as your password is stronger.',
'Dont use common number patterns like 111111, abc123, or 654321.',
'Adding a number or piece of punctuation at the end doesnt make your password stronger.'
]
},
{
subhead: "One exposed password can unlock many accounts.",
subhead: 'One exposed password can unlock many accounts.',
paragraphs: [
"Hackers know people reuse the same passwords. If your banking password is the same as your email password is the same as your Amazon password, a single vulnerability in one site can put the others at risk.",
"Its why you should use different passwords for every single account. The average person has 90 accounts, and thats a lot of passwords to remember. Security experts recommend using a password manager to safely store unique passwords for every site.",
],
},
{
subhead: "Hackers dont care how much money you have.",
paragraphs: [
"Think you dont need to worry because you dont have much money to steal? Hackers couldnt care less. There are countless ways to leverage all types of personal data for profit.",
"Through identity theft, cyber criminals can open new credit cards or apply for loans in your name. By getting your financial information, they can make purchases or withdrawals. These attackers can even find ways to target your friends and family once they gain access to your email.",
],
'Hackers know people reuse the same passwords. If your banking password is the same as your email password is the same as your Amazon password, a single vulnerability in one site can put the others at risk.',
'Its why you should use different passwords for every single account. The average person has 90 accounts, and thats a lot of passwords to remember. Security experts recommend using a password manager to safely store unique passwords for every site.'
]
},
],
{
subhead: 'Hackers dont care how much money you have.',
paragraphs: [
'Think you dont need to worry because you dont have much money to steal? Hackers couldnt care less. There are countless ways to leverage all types of personal data for profit.',
'Through identity theft, cyber criminals can open new credit cards or apply for loans in your name. By getting your financial information, they can make purchases or withdrawals. These attackers can even find ways to target your friends and family once they gain access to your email.'
]
}
]
},
"after-breach": {
'after-breach': {
paragraphs: [
{
leads: [
"You get an email, either from Firefox Monitor or a company where you have an account. Theres been a security incident. Your account has been compromised.",
'You get an email, either from Firefox Monitor or a company where you have an account. Theres been a security incident. Your account has been compromised.'
],
paragraphs: [
"Getting notified that youve been a victim of a data breach can be alarming. You have valid cause for concern, but there are a <span class='bold'>few steps you can take immediately to protect your account and limit the damage.</span>",
],
},
{
toggles: [
{
subhead: "Read the details about the breach.",
paragraphs: [
"Read closely to learn what happened. What personal data of yours was included? Your next steps will depend on what information you need to protect. When did the breach happen? You may receive the notice months or even years after the data breach occurred. Sometimes it takes awhile for companies to discover a breach. Sometimes breaches are not immediately made public.",
],
},
{
subhead: "If you havent yet, change your password.",
paragraphs: [
"Lock down your account with a new password. If you cant log in, contact the website to ask how you can recover or shut down the account. See an account you dont recognize? The site may have changed names or someone may have created an account for you.",
],
},
{
subhead: "If youve used that password for other accounts, change those too.",
paragraphs: [
"Hackers may try to reuse your exposed password to get into other accounts. Create a different password for each website, especially for your financial accounts, email account, and other websites where you save personal information.",
],
},
{
subhead: "Take extra steps if your financial data was breached.",
paragraphs: [
"Many breaches expose emails and passwords, but some do include sensitive financial information. If your bank account or credit card numbers were included in a breach, alert your bank to possible fraud. Monitor statements for charges you don't recognize.",
],
},
{
subhead: "Review your credit reports to catch identity theft.",
paragraphs: [
"If you have credit history in the United States, check your credit reports for suspicious activity. Ensure that no new accounts, loans, or cards have been opened in your name. By law, youre permitted to one free report from the three major credit reporting bureaus every year. Request them through annualcreditreport.com. And dont worry, checking your own credit report never affects your score.",
],
},
],
},
]},
"strong-passwords": {
paragraphs: [
{
leads: [
"Your password is your first line of defense against hackers and unauthorized access to your accounts. The strength of your passwords directly impacts your online security.",
],
"Getting notified that youve been a victim of a data breach can be alarming. You have valid cause for concern, but there are a <span class='bold'>few steps you can take immediately to protect your account and limit the damage.</span>"
]
},
{
toggles: [
{
subhead: "Combine unrelated words to make stronger passwords.",
subhead: 'Read the details about the breach.',
paragraphs: [
"To create a strong password, try combining two or more unrelated words. It could even be an entire phrase. Then change some of the letters to special letters and numbers. The longer your password, the stronger it is.",
"A single word with one letter changed to an @ or ! (such as p@ssword!) doesnt make for a strong password. Password cracking programs contain every type of these combinations, in every single language.",
],
'Read closely to learn what happened. What personal data of yours was included? Your next steps will depend on what information you need to protect. When did the breach happen? You may receive the notice months or even years after the data breach occurred. Sometimes it takes awhile for companies to discover a breach. Sometimes breaches are not immediately made public.'
]
},
{
subhead: "Certain words should be avoided in all passwords.",
subhead: 'If you havent yet, change your password.',
paragraphs: [
"Many people use familiar people, places, or things in passwords because it makes their passwords easy to remember. This also makes your passwords easy for hackers to guess.",
"According to a study <a target='_blank' rel='noopener noreferrer' href='https://techland.time.com/2013/08/08/google-reveals-the-10-worst-password-ideas/'>conducted by Google</a>, passwords that contain the following information are considered insecure because theyre easy to figure out. You can find much of this info after reviewing someones social media profiles.",
'Lock down your account with a new password. If you cant log in, contact the website to ask how you can recover or shut down the account. See an account you dont recognize? The site may have changed names or someone may have created an account for you.'
]
},
{
subhead: 'If youve used that password for other accounts, change those too.',
paragraphs: [
'Hackers may try to reuse your exposed password to get into other accounts. Create a different password for each website, especially for your financial accounts, email account, and other websites where you save personal information.'
]
},
{
subhead: 'Take extra steps if your financial data was breached.',
paragraphs: [
"Many breaches expose emails and passwords, but some do include sensitive financial information. If your bank account or credit card numbers were included in a breach, alert your bank to possible fraud. Monitor statements for charges you don't recognize."
]
},
{
subhead: 'Review your credit reports to catch identity theft.',
paragraphs: [
'If you have credit history in the United States, check your credit reports for suspicious activity. Ensure that no new accounts, loans, or cards have been opened in your name. By law, youre permitted to one free report from the three major credit reporting bureaus every year. Request them through annualcreditreport.com. And dont worry, checking your own credit report never affects your score.'
]
}
]
}
]
},
'strong-passwords': {
paragraphs: [
{
leads: [
'Your password is your first line of defense against hackers and unauthorized access to your accounts. The strength of your passwords directly impacts your online security.'
]
},
{
toggles: [
{
subhead: 'Combine unrelated words to make stronger passwords.',
paragraphs: [
'To create a strong password, try combining two or more unrelated words. It could even be an entire phrase. Then change some of the letters to special letters and numbers. The longer your password, the stronger it is.',
'A single word with one letter changed to an @ or ! (such as p@ssword!) doesnt make for a strong password. Password cracking programs contain every type of these combinations, in every single language.'
]
},
{
subhead: 'Certain words should be avoided in all passwords.',
paragraphs: [
'Many people use familiar people, places, or things in passwords because it makes their passwords easy to remember. This also makes your passwords easy for hackers to guess.',
"According to a study <a target='_blank' rel='noopener noreferrer' href='https://techland.time.com/2013/08/08/google-reveals-the-10-worst-password-ideas/'>conducted by Google</a>, passwords that contain the following information are considered insecure because theyre easy to figure out. You can find much of this info after reviewing someones social media profiles."
],
list: [
"Pet names",
"A notable date, such as a wedding anniversary",
"A family members birthday",
"Your childs name",
"Another family members name",
"Your birthplace",
"A favorite holiday",
"Something related to your favorite sports team",
"The name of a significant other",
"The word “Password,” or any variation of it. That includes P@assword!",
'Pet names',
'A notable date, such as a wedding anniversary',
'A family members birthday',
'Your childs name',
'Another family members name',
'Your birthplace',
'A favorite holiday',
'Something related to your favorite sports team',
'The name of a significant other',
'The word “Password,” or any variation of it. That includes P@assword!'
],
securityTip: {
tipHeadline: "<span class='bold'>SECURITY TIP</span> Steer clear of the 100 most-used passwords.",
tipSubhead: "Every year, SplashData evaluates millions of leaked passwords and compiles the <a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://www.teamsid.com/splashdatas-top-100-worst-passwords-of-2018/'>100 most common ones.</a> The most recent list includes password, 123456, and other passwords you shouldnt use. ",
},
tipSubhead: "Every year, SplashData evaluates millions of leaked passwords and compiles the <a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://www.teamsid.com/splashdatas-top-100-worst-passwords-of-2018/'>100 most common ones.</a> The most recent list includes password, 123456, and other passwords you shouldnt use. "
}
},
{
subhead: "Use different passwords for every account.",
subhead: 'Use different passwords for every account.',
paragraphs: [
"To keep your accounts as secure as possible, its best that every single one has a unique password. If one account gets breached, then hackers cant use those login credentials to gain access to other accounts.",
"While no one can stop hackers from hacking, you can stop reusing the same password everywhere. It makes it far too easy for cyber criminals to attack one site and get your password for others.",
],
'To keep your accounts as secure as possible, its best that every single one has a unique password. If one account gets breached, then hackers cant use those login credentials to gain access to other accounts.',
'While no one can stop hackers from hacking, you can stop reusing the same password everywhere. It makes it far too easy for cyber criminals to attack one site and get your password for others.'
]
},
{
subhead: "Use a password manager to remember all your passwords.",
subhead: 'Use a password manager to remember all your passwords.',
paragraphs: [
"Do you really need to remember 100 passwords? Not at all. A password manager is a piece of software that keeps all your password safe, encrypted, and protected. It can even generate strong passwords for you and automatically enter them in to websites and apps.",
"Password managers act like a digital safe-deposit box for all your online accounts. You just need one key to get into your accounts: A single, easy-to-remember but hard-to-guess password. That password unlocks the safe.",
"But what if your password manager gets hacked? A good one keeps your passwords encrypted behind a password they dont know (only you do). They dont store any of your credentials on their servers. While no single tool can guarantee total online safety, security experts agree that using a password manager is far more secure than using the same password everywhere.",
],
},
{
subhead: "Add an extra layer of security with two-factor authentication.",
paragraphs: [
"Many websites offer two-factor authentication, also known as 2FA or multi-factor authentication. On top of your username and password, 2FA requires another piece of information to verify yourself. So, even if someone has your password, they cant get in.",
"Withdrawing money from an ATM is an example of 2FA. It requires your PIN code and your bank card. You need these two pieces to complete the transaction.",
"<a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://2fa.directory/'>Websites that support 2FA</a> include Google and Amazon. When you have 2FA enabled, the site will text you a code to enter after your password. Other forms of 2FA include YubiKeys USB ports and security apps like DUO. ",
"When you set up 2FA, many sites will give you a list of backup codes to verify your account. A password manager is a great place to store these codes.",
],
},
],
},
{
passwordDosAndDonts: {
listHeadline: "Password dos and donts",
doDontList : [
{
do: "Do use different passwords everywhere. Password managers and many browsers can generate secure and unique passwords.",
dont: "Dont use variations of the same password for different accounts.",
},
{
do: "Do combine two or more unrelated words. Change letters to numbers or special characters.",
dont: "Dont use the word “password,” or any variation of it. “P@ssword!” is just as easy for hackers to guess.",
},
{
do: "Do make your passwords at least 8 characters long. Aim for 12-15 characters.",
dont: "Don't use short, one-word passwords, like sunshine, monkey, or football.",
},
{
do: "Do intersperse numbers, symbols, and special characters throughout.",
dont: "Dont place special characters (@, !, 0, etc.) only at the beginning or the end.",
},
{
do: "Do include unusual words only you would know. It should seem nonsensical to other people.",
dont: "Dont include personal information like your birthdate, address, or family members names.",
},
{
do: "Do keep your passwords protected and safe, like encrypted in a password manager.",
dont: "Dont share your passwords. Dont put them on a piece of paper stuck to your computer.",
},
{
do: "Do spread various numbers and characters throughout your password.",
dont: "Dont use common patterns like 111111, abc123, or 654321.",
},
{
do: "Do use an extra layer of security with two-factor authentication (2FA).",
dont: "Dont think a weaker password is safer because you have 2FA.",
},
],
},
'Do you really need to remember 100 passwords? Not at all. A password manager is a piece of software that keeps all your password safe, encrypted, and protected. It can even generate strong passwords for you and automatically enter them in to websites and apps.',
'Password managers act like a digital safe-deposit box for all your online accounts. You just need one key to get into your accounts: A single, easy-to-remember but hard-to-guess password. That password unlocks the safe.',
'But what if your password manager gets hacked? A good one keeps your passwords encrypted behind a password they dont know (only you do). They dont store any of your credentials on their servers. While no single tool can guarantee total online safety, security experts agree that using a password manager is far more secure than using the same password everywhere.'
]
},
{
subhead: 'Add an extra layer of security with two-factor authentication.',
paragraphs: [
'Many websites offer two-factor authentication, also known as 2FA or multi-factor authentication. On top of your username and password, 2FA requires another piece of information to verify yourself. So, even if someone has your password, they cant get in.',
'Withdrawing money from an ATM is an example of 2FA. It requires your PIN code and your bank card. You need these two pieces to complete the transaction.',
"<a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://2fa.directory/'>Websites that support 2FA</a> include Google and Amazon. When you have 2FA enabled, the site will text you a code to enter after your password. Other forms of 2FA include YubiKeys USB ports and security apps like DUO. ",
'When you set up 2FA, many sites will give you a list of backup codes to verify your account. A password manager is a great place to store these codes.'
]
}
]
},
],
{
passwordDosAndDonts: {
listHeadline: 'Password dos and donts',
doDontList: [
{
do: 'Do use different passwords everywhere. Password managers and many browsers can generate secure and unique passwords.',
dont: 'Dont use variations of the same password for different accounts.'
},
{
do: 'Do combine two or more unrelated words. Change letters to numbers or special characters.',
dont: 'Dont use the word “password,” or any variation of it. “P@ssword!” is just as easy for hackers to guess.'
},
{
do: 'Do make your passwords at least 8 characters long. Aim for 12-15 characters.',
dont: "Don't use short, one-word passwords, like sunshine, monkey, or football."
},
{
do: 'Do intersperse numbers, symbols, and special characters throughout.',
dont: 'Dont place special characters (@, !, 0, etc.) only at the beginning or the end.'
},
{
do: 'Do include unusual words only you would know. It should seem nonsensical to other people.',
dont: 'Dont include personal information like your birthdate, address, or family members names.'
},
{
do: 'Do keep your passwords protected and safe, like encrypted in a password manager.',
dont: 'Dont share your passwords. Dont put them on a piece of paper stuck to your computer.'
},
{
do: 'Do spread various numbers and characters throughout your password.',
dont: 'Dont use common patterns like 111111, abc123, or 654321.'
},
{
do: 'Do use an extra layer of security with two-factor authentication (2FA).',
dont: 'Dont think a weaker password is safer because you have 2FA.'
}
]
}
}
]
},
"steps-to-protect": {
'steps-to-protect': {
paragraphs: [
{
leads: [
"Data breaches are one of many online threats. Using secure internet connections, updating your software, avoiding scam emails, and employing better password hygiene will help you stay safer while you browse.",
],
'Data breaches are one of many online threats. Using secure internet connections, updating your software, avoiding scam emails, and employing better password hygiene will help you stay safer while you browse.'
]
},
{
toggles: [
{
subhead: "Be wary of public Wi-Fi networks.",
subhead: 'Be wary of public Wi-Fi networks.',
paragraphs: [
"You can get Wi-Fi almost anywhere. But these open networks are the most vulnerable and tend to be the least secure. This includes the free Wi-Fi at restaurants, libraries, airports, and other public spaces. If you can avoid it, dont use public Wi-Fi. Most importantly, dont use these networks to log in to financial sites or shop online. Its easy for anyone to see what youre doing.",
"Instead, we recommend using a Virtual Private Network (like <a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>), which lets you use public Wi-Fi more securely and keeps your online behavior private. A VPN routes your connection through a secure server that encrypts your data before you land on a web page. ",
],
'You can get Wi-Fi almost anywhere. But these open networks are the most vulnerable and tend to be the least secure. This includes the free Wi-Fi at restaurants, libraries, airports, and other public spaces. If you can avoid it, dont use public Wi-Fi. Most importantly, dont use these networks to log in to financial sites or shop online. Its easy for anyone to see what youre doing.',
"Instead, we recommend using a Virtual Private Network (like <a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>), which lets you use public Wi-Fi more securely and keeps your online behavior private. A VPN routes your connection through a secure server that encrypts your data before you land on a web page. "
]
},
{
subhead: "Run software and app updates as soon as theyre available.",
subhead: 'Run software and app updates as soon as theyre available.',
paragraphs: [
"Updating software on your computer or phone can seem like a pain, but its a crucial step to keeping devices safe. These updates fix bugs, software vulnerabilities, and security problems. Regularly updating your smartphone apps and operating systems makes your devices more secure.",
'Updating software on your computer or phone can seem like a pain, but its a crucial step to keeping devices safe. These updates fix bugs, software vulnerabilities, and security problems. Regularly updating your smartphone apps and operating systems makes your devices more secure.'
],
listHeadline: "Tips for keeping all your online accounts secure",
listHeadline: 'Tips for keeping all your online accounts secure',
list: [
"Use unique, strong passwords for every account",
"Use a password manager to remember all your passwords for you",
"Turn on two-factor authentication for an extra layer of security",
'Use unique, strong passwords for every account',
'Use a password manager to remember all your passwords for you',
'Turn on two-factor authentication for an extra layer of security',
"Use a VPN (like&nbsp;<a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>) when using public Wi-Fi",
"Update to the latest version of all software and apps",
],
'Update to the latest version of all software and apps'
]
},
{
securityTip: {
tipHeadline: "<span class='bold'>SECURITY TIP</span> Turn on automatic updates.",
tipSubhead: "You can set your computer, browser, apps, and phone to update automatically as soon as new updates become available. Set it and forget it!",
},
tipSubhead: 'You can set your computer, browser, apps, and phone to update automatically as soon as new updates become available. Set it and forget it!'
}
},
{
subhead: "Be vigilant about emails that seem even a little bit strange.",
subhead: 'Be vigilant about emails that seem even a little bit strange.',
paragraphs: [
"Phishing is a type of email scam that is becoming increasingly common. In these emails, hackers impersonate a service or company you trust. These emails can even come from one of your contacts. They look like the real thing because they mimic the design of authentic emails, like those from your bank or email provider.",
"The goal of these hackers is to get you to unknowingly enter your password or download a document that can infect your computer. Most online services wont ask you to enter your login info directly from an email. If they do, you should instead go directly to their website to log in.",
"Think before you fill anything out. Does this email seem out of the blue? Does something seem off about it? Are you being asked to log in to an account to update something? Dont click, and dont enter your password anywhere. Open your browser, and type in the address of the company website instead.",
'Phishing is a type of email scam that is becoming increasingly common. In these emails, hackers impersonate a service or company you trust. These emails can even come from one of your contacts. They look like the real thing because they mimic the design of authentic emails, like those from your bank or email provider.',
'The goal of these hackers is to get you to unknowingly enter your password or download a document that can infect your computer. Most online services wont ask you to enter your login info directly from an email. If they do, you should instead go directly to their website to log in.',
'Think before you fill anything out. Does this email seem out of the blue? Does something seem off about it? Are you being asked to log in to an account to update something? Dont click, and dont enter your password anywhere. Open your browser, and type in the address of the company website instead.'
],
listHeadline: "Classic signs of a suspicious email",
listHeadline: 'Classic signs of a suspicious email',
list: [
"Displays grammar or spelling mistakes",
"Seems especially urgent or time-critical",
"Send address looks unusual",
"Promises something that seems too good to be true",
"Asks you to log in from the email itself",
"Asks you to open or download a file that you dont recognize",
'Displays grammar or spelling mistakes',
'Seems especially urgent or time-critical',
'Send address looks unusual',
'Promises something that seems too good to be true',
'Asks you to log in from the email itself',
'Asks you to open or download a file that you dont recognize'
],
warning: "Clicked on a phishing email? Contact your email administrator right away for next steps. Do this before sending or opening any other emails. The faster you report it, the faster the damage can be mitigated.",
warning: 'Clicked on a phishing email? Contact your email administrator right away for next steps. Do this before sending or opening any other emails. The faster you report it, the faster the damage can be mitigated.'
},
{
subhead: "Be selective about who you give your email address to.",
subhead: 'Be selective about who you give your email address to.',
paragraphs: [
"The more online accounts you create, the greater the risk that youll be involved in a data breach. Many companies, services, apps, and websites ask for your email. But its not always required. Here are some ways to avoid giving out your email address:",
'The more online accounts you create, the greater the risk that youll be involved in a data breach. Many companies, services, apps, and websites ask for your email. But its not always required. Here are some ways to avoid giving out your email address:'
],
list: [
"Dont create an account if its not required. For example, many online shopping portals allow you to check out as a guest.",
"If a website requires an email address, use services like 10minutemail or Nada, which allow you to create a temporary one.",
"Create a different email to sign up for promotions and newsletters. Dont include any personal info that could be used to identify you in that email address, like your name or birthday.",
],
'Dont create an account if its not required. For example, many online shopping portals allow you to check out as a guest.',
'If a website requires an email address, use services like 10minutemail or Nada, which allow you to create a temporary one.',
'Create a different email to sign up for promotions and newsletters. Dont include any personal info that could be used to identify you in that email address, like your name or birthday.'
]
},
{
subhead: "Use unique, strong passwords for every single account.",
subhead: 'Use unique, strong passwords for every single account.',
paragraphs: [
"One of the best ways to protect yourself online is to use different passwords across all your online accounts. This way, hackers wont have the keys to your entire digital life if they get their hands on that one password you use everywhere.",
"Your passwords also need to be strong. Single words (like sunshine, monkey, or football) make for weak passwords. So do these 100 most-commonly used passwords, which include password and 123456. Avoid pop-culture references, sports teams, and personal info. Do not use your address, birthday, names of family members, or pets names. The longer and more unique your passwords are, the harder they will be for hackers to crack.",
'One of the best ways to protect yourself online is to use different passwords across all your online accounts. This way, hackers wont have the keys to your entire digital life if they get their hands on that one password you use everywhere.',
'Your passwords also need to be strong. Single words (like sunshine, monkey, or football) make for weak passwords. So do these 100 most-commonly used passwords, which include password and 123456. Avoid pop-culture references, sports teams, and personal info. Do not use your address, birthday, names of family members, or pets names. The longer and more unique your passwords are, the harder they will be for hackers to crack.'
],
securityTip: {
tipHeadline: "<span class='bold'>SECURITY TIP</span> How to create strong passwords",
tipSubhead: "Include a combination of upper and lowercase letters, numbers, and characters. Combining a few unrelated words and changing the letters is a good method. <a class='st-link' href='#strong-passwords'>Read the guide</a>",
},
tipSubhead: "Include a combination of upper and lowercase letters, numbers, and characters. Combining a few unrelated words and changing the letters is a good method. <a class='st-link' href='#strong-passwords'>Read the guide</a>"
}
},
{
subhead: "Remember all your passwords with a password manager.",
subhead: 'Remember all your passwords with a password manager.',
paragraphs: [
"Ever forgotten your password? It happens all the time. The average person has 90 online accounts. And were being asked to create new ones all the time.",
"The good news is you dont have to recall all your passwords from memory. Password managers are secure, easy-to-use applications that do the remembering for you. They even fill your passwords into websites and apps when you need to log in. All you need to remember is a single password — the one you use to unlock your password manager. They can even generate hard-to-guess passwords to help make your accounts more secure. All your data is encrypted, making password managers pretty secure — even if they get hacked.",
],
'Ever forgotten your password? It happens all the time. The average person has 90 online accounts. And were being asked to create new ones all the time.',
'The good news is you dont have to recall all your passwords from memory. Password managers are secure, easy-to-use applications that do the remembering for you. They even fill your passwords into websites and apps when you need to log in. All you need to remember is a single password — the one you use to unlock your password manager. They can even generate hard-to-guess passwords to help make your accounts more secure. All your data is encrypted, making password managers pretty secure — even if they get hacked.'
],
securityTip: {
tipHeadline: "<span class='bold'>SECURITY TIP</span>",
tipSubhead: "Firefox recommends 1Password, LastPass, Dashlane, and Bitwarden for security and ease of use.",
},
tipSubhead: 'Firefox recommends 1Password, LastPass, Dashlane, and Bitwarden for security and ease of use.'
}
},
{
securityTip: {
tipHeadline: "<span class='bold'>SECURITY TIP</span>",
tipSubhead: "Still wary of password managers? Whats most important is that you use different passwords everywhere. To remember them, write down your passwords and store them in a safe place that only you have access to.",
},
},
],
},
],
tipSubhead: 'Still wary of password managers? Whats most important is that you use different passwords everywhere. To remember them, write down your passwords and store them in a safe place that only you have access to.'
}
}
]
}
]
},
"five-myths": {
'five-myths': {
paragraphs: [
{
leads: [
"Password managers are the most recommended tool by security experts to protect your online credentials from hackers. But many people are still hesitant to use them. Heres why password managers are safe, secure, and your best defense against password-hungry cyber criminals.",
],
'Password managers are the most recommended tool by security experts to protect your online credentials from hackers. But many people are still hesitant to use them. Heres why password managers are safe, secure, and your best defense against password-hungry cyber criminals.'
]
},
{
subhead: 'What is a password manager?',
paragraphs: [
'Think of it like a safe for your passwords. When you need something inside the safe, you unlock it. Password managers work the same for your online credentials.',
'You create a single, super-strong password, which acts like a key. Install the password manager app on your phone, computer, browser, and other devices. Your passwords are securely stored inside. Anytime you need to log in to an account, unlock your password manager and retrieve your login info.'
]
},
{
subhead: "What is a password manager?",
paragraphs: [
"Think of it like a safe for your passwords. When you need something inside the safe, you unlock it. Password managers work the same for your online credentials.",
"You create a single, super-strong password, which acts like a key. Install the password manager app on your phone, computer, browser, and other devices. Your passwords are securely stored inside. Anytime you need to log in to an account, unlock your password manager and retrieve your login info.",
],
},
{
toggles: [
{
subhead: "<span class='myth'>Myth 1:</span> Password managers arent safe or trustworthy.",
paragraphs: [
"With website vulnerabilities and security incidents on the rise, many people have grown to mistrust a tech tool to manage their passwords. What if the password manager gets hacked?",
"Reputable password managers take extra steps to lock down your info and keep it safe from cyber criminals.",
'With website vulnerabilities and security incidents on the rise, many people have grown to mistrust a tech tool to manage their passwords. What if the password manager gets hacked?',
'Reputable password managers take extra steps to lock down your info and keep it safe from cyber criminals.'
],
listHeadline: "A good password manager:",
listHeadline: 'A good password manager:',
list: [
"Doesnt know your primary password (so hackers can never steal it)",
"Only stores encrypted versions of your credentials and data on their servers ",
"Does not store any of your data on their servers",
"Can generate strong, secure password",
],
},
],
'Doesnt know your primary password (so hackers can never steal it)',
'Only stores encrypted versions of your credentials and data on their servers ',
'Does not store any of your data on their servers',
'Can generate strong, secure password'
]
}
]
},
{
toggles: [
{
subhead: "<span class='myth'>Myth 2:</span> Password managers arent 100% secure, so I shouldnt use one.",
paragraphs: [
"No tool can completely guarantee your online safety. Even the most elaborate lock can be broken into. Yet we still lock our doors to our houses and cars. ",
"The alternative to using a password manager is to rely on your own memory to remember all your credentials. This inevitably leads to recycling passwords or using variations — a bad habit that hackers love.",
"Password managers can be such an effective security tool because they help us improve bad habits. With a password manager installed on your computer and phone, its a lot easier to take your logins everywhere so you can use unique, strong passwords on every account.",
],
'No tool can completely guarantee your online safety. Even the most elaborate lock can be broken into. Yet we still lock our doors to our houses and cars. ',
'The alternative to using a password manager is to rely on your own memory to remember all your credentials. This inevitably leads to recycling passwords or using variations — a bad habit that hackers love.',
'Password managers can be such an effective security tool because they help us improve bad habits. With a password manager installed on your computer and phone, its a lot easier to take your logins everywhere so you can use unique, strong passwords on every account.'
]
},
{
subhead: "<span class='myth'>Myth 3:</span> Storing all my passwords in one place makes them vulnerable to hackers.",
paragraphs: [
"Password managers dont store all your credentials together in one place. Any data you store in a password manager — passwords, logins, security questions, and other sensitive info — is securely encrypted. Even if the password manager gets hacked, cyber criminals would not be able to see your logins.",
"The only way to access your data is with a single primary password that only you know. You use this password to unlock the manager on your computer, phone, or other devices. Once its unlocked, a password manager can fill in your logins to websites and apps.",
],
'Password managers dont store all your credentials together in one place. Any data you store in a password manager — passwords, logins, security questions, and other sensitive info — is securely encrypted. Even if the password manager gets hacked, cyber criminals would not be able to see your logins.',
'The only way to access your data is with a single primary password that only you know. You use this password to unlock the manager on your computer, phone, or other devices. Once its unlocked, a password manager can fill in your logins to websites and apps.'
]
},
{
subhead: "<span class='myth'>Myth 4:</span> Remembering all my passwords is safer than trusting technology to do it for me.",
paragraphs: [
"Our memories sometimes fail us. Ever clicked a “forgot password?” link? Its very common to use variations of the same password to make them easier to remember. With a password manager, you dont need to remember any of your credentials. It can be installed on all your devices and will auto-fill your passwords for you. Once you get in the habit of using one, youll no longer have to worry about forgetting your credentials.",
],
'Our memories sometimes fail us. Ever clicked a “forgot password?” link? Its very common to use variations of the same password to make them easier to remember. With a password manager, you dont need to remember any of your credentials. It can be installed on all your devices and will auto-fill your passwords for you. Once you get in the habit of using one, youll no longer have to worry about forgetting your credentials.'
]
},
{
subhead: "<span class='myth'>Myth 5:</span> Its a huge pain to set up a password manager.",
paragraphs: [
"Sure, it takes time to log all your credentials in a password manager. But you dont need to do it all at once. You can always start small and change just a few passwords at a time. Try installing a password manager and creating new, unique passwords for the websites you visit most frequently. Over time, as you log in to other sites, you can add others.",
],
},
],
},
],
'Sure, it takes time to log all your credentials in a password manager. But you dont need to do it all at once. You can always start small and change just a few passwords at a time. Try installing a password manager and creating new, unique passwords for the websites you visit most frequently. Over time, as you log in to other sites, you can add others.'
]
}
]
}
]
},
"next-steps": {
'next-steps': {
paragraphs: [
{
leads: [
"When significant data breaches happen where high-risk data is at stake, theres often talk about credit reports. Some companies may even be required to provide credit monitoring as part of its breach notification requirements. Security experts recommend you check your credit reports for suspicious activity. To protect your identity, they also recommend you freeze your credit. Heres what that means and why its important.",
'When significant data breaches happen where high-risk data is at stake, theres often talk about credit reports. Some companies may even be required to provide credit monitoring as part of its breach notification requirements. Security experts recommend you check your credit reports for suspicious activity. To protect your identity, they also recommend you freeze your credit. Heres what that means and why its important.'
],
subhead: "Whats a credit report? Do I have one?",
subhead: 'Whats a credit report? Do I have one?',
paragraphs: [
"If youve ever rented an apartment, opened a bank account, or applied for a credit card or a loan, you likely have a credit report.",
"In fact, you have three credit reports. There are three credit-reporting bureaus in the United States: Experian, TransUnion, and Equifax. Each one holds a report on you that contains personal information about your credit history. Your credit reports contain:",
],
'If youve ever rented an apartment, opened a bank account, or applied for a credit card or a loan, you likely have a credit report.',
'In fact, you have three credit reports. There are three credit-reporting bureaus in the United States: Experian, TransUnion, and Equifax. Each one holds a report on you that contains personal information about your credit history. Your credit reports contain:'
]
},
{
list: [
"Personal identifying information, such as your name, past and current addresses, Social Security number, and date of birth.",
"Current and past credit accounts, such as credit cards, mortgages, student loans, and auto loans.",
"Inquiry information, which are instances in which youve applied for new loans or credit cards.",
"Bankruptcies and collection information.",
"Your credit report does not include your credit score.",
],
'Personal identifying information, such as your name, past and current addresses, Social Security number, and date of birth.',
'Current and past credit accounts, such as credit cards, mortgages, student loans, and auto loans.',
'Inquiry information, which are instances in which youve applied for new loans or credit cards.',
'Bankruptcies and collection information.',
'Your credit report does not include your credit score.'
]
},
{
toggles: [
{
subhead: "Why you should check your credit reports once a year.",
subhead: 'Why you should check your credit reports once a year.',
paragraphs: [
"Having your information exposed in a data breach puts you at risk of identity theft. If someone steals your identity and tries to open new cards or loans in your name, it will appear on your credit reports. Each bureau may have slightly different information, which is why its important to check all three regularly.",
"By law, you are entitled to one free credit report a year from each of the three credit bureaus. You can request your credit reports at annualcreditreport.com. This is the only official and truly free website to obtain your reports. You can also call Experian, TransUnion, and Equifax directly or request your reports by mail.",
],
},
],
'Having your information exposed in a data breach puts you at risk of identity theft. If someone steals your identity and tries to open new cards or loans in your name, it will appear on your credit reports. Each bureau may have slightly different information, which is why its important to check all three regularly.',
'By law, you are entitled to one free credit report a year from each of the three credit bureaus. You can request your credit reports at annualcreditreport.com. This is the only official and truly free website to obtain your reports. You can also call Experian, TransUnion, and Equifax directly or request your reports by mail.'
]
}
]
},
{
subhead: "Checking your own credit report will not affect your score.",
subhead: 'Checking your own credit report will not affect your score.',
paragraphs: [
"You will never be penalized for checking your own report or your own credit score. Checking your report does not impact your score in any way. Experian, TransUnion, and Equifax may offer paid identity monitoring packages or charge for access to your credit score, but its always free to check your report once a year.",
"Though the information on your credit report directly impacts your score, reports dont actually contain your score. There are many websites, services, and credit cards where you can check your score for free. So its usually not necessary to pay the bureaus themselves to see your score.",
],
'You will never be penalized for checking your own report or your own credit score. Checking your report does not impact your score in any way. Experian, TransUnion, and Equifax may offer paid identity monitoring packages or charge for access to your credit score, but its always free to check your report once a year.',
'Though the information on your credit report directly impacts your score, reports dont actually contain your score. There are many websites, services, and credit cards where you can check your score for free. So its usually not necessary to pay the bureaus themselves to see your score.'
]
},
{
toggles: [
{
subhead: "What to look for to spot signs of identity theft.",
subhead: 'What to look for to spot signs of identity theft.',
paragraphs: [
"When you receive your credit reports from Experian, TransUnion, and Equifax, review them carefully. These are long, dense documents that can be overwhelming, especially if you have a long credit history. You have a right to correct any inaccurate information on your record with the credit bureau. Make sure:",
'When you receive your credit reports from Experian, TransUnion, and Equifax, review them carefully. These are long, dense documents that can be overwhelming, especially if you have a long credit history. You have a right to correct any inaccurate information on your record with the credit bureau. Make sure:'
],
list: [
"All the accounts listed are ones you personally opened.",
"All addresses listed and your employer are correct.",
"Your balances and credit history are correct.",
"All hard credit inquiries are from loans or credit cards you applied for. Soft inquiries may be listed, which are from pre-approved credit card offers. These do not affect your score.",
"If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. Instructions for how to initiate corrections can be found on your report. Its important not to let inaccuracies linger as they may lower your score or be difficult to clear later. If you are concerned that you might be a victim of identity theft, report it and get help from the Federal Trade Commision at identitytheft.gov.",
'All the accounts listed are ones you personally opened.',
'All addresses listed and your employer are correct.',
'Your balances and credit history are correct.',
'All hard credit inquiries are from loans or credit cards you applied for. Soft inquiries may be listed, which are from pre-approved credit card offers. These do not affect your score.',
'If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. Instructions for how to initiate corrections can be found on your report. Its important not to let inaccuracies linger as they may lower your score or be difficult to clear later. If you are concerned that you might be a victim of identity theft, report it and get help from the Federal Trade Commision at identitytheft.gov.'
],
warning: [
"If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. All have processes by which you can dispute inaccuracies by mail or online and get information corrected. ",
],
},
],
'If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. All have processes by which you can dispute inaccuracies by mail or online and get information corrected. '
]
}
]
},
{
subhead: "Next step: Block unauthorized access to your credit report with a credit freeze. ",
subhead: 'Next step: Block unauthorized access to your credit report with a credit freeze. ',
paragraphs: [
"Placing a freeze on your credit report is the most effective method to stop identity thieves in their tracks. Its completely free with all three bureaus and will not affect your credit cards, credit report, or credit score. You can continue using your cards as you were before.",
"Freezing your credit report means only you can apply for new cards or loans. No one else will be able to do this in your name. Its like putting a lock on your credit report, and only you have the key. You can unlock (or unfreeze) your credit report at any time. For example, you may want to open a new credit card. You can temporarily lift the freeze to do so, then refreeze your credit report again after.",
"Federal legislation requires credit-reporting agencies to offer free credit freezes and unfreezes. To freeze your credit report with Experian, TransUnion, and Equifax, call them directly or do it on their websites. You may be asked to create a PIN code or they may generate one for you. Keep this code safe, because its the one youll use if you need to unlock your credit. A password manager is a great place to save your PIN codes.",
],
},
],
},
'Placing a freeze on your credit report is the most effective method to stop identity thieves in their tracks. Its completely free with all three bureaus and will not affect your credit cards, credit report, or credit score. You can continue using your cards as you were before.',
'Freezing your credit report means only you can apply for new cards or loans. No one else will be able to do this in your name. Its like putting a lock on your credit report, and only you have the key. You can unlock (or unfreeze) your credit report at any time. For example, you may want to open a new credit card. You can temporarily lift the freeze to do so, then refreeze your credit report again after.',
'Federal legislation requires credit-reporting agencies to offer free credit freezes and unfreezes. To freeze your credit report with Experian, TransUnion, and Equifax, call them directly or do it on their websites. You may be asked to create a PIN code or they may generate one for you. Keep this code safe, because its the one youll use if you need to unlock your credit. A password manager is a great place to save your PIN codes.'
]
}
]
}
};
function getArticleCopy(args) {
const articles = articleLinks(args);
articles.forEach(article => {
const articleName = article["class"];
article["copy"] = articleCopy[articleName].paragraphs;
});
return articles;
}
function articleLinks(args) {
const locales = args.data.root.req.supportedLocales;
function getArticleCopy (args) {
const articles = articleLinks(args)
articles.forEach(article => {
const articleName = article.class
article.copy = articleCopy[articleName].paragraphs
})
return articles
}
function articleLinks (args) {
const locales = args.data.root.req.supportedLocales
const articleLinks = [
{
title: "Understand how hackers work",
stringId: LocaleUtils.fluentFormat(locales, "how-hackers-work"),
class: "how-hackers-work",
pathToPartial: "svg/icon-hackers",
subhead: LocaleUtils.fluentFormat(locales, "how-hackers-work-desc"),
title: 'Understand how hackers work',
stringId: LocaleUtils.fluentFormat(locales, 'how-hackers-work'),
class: 'how-hackers-work',
pathToPartial: 'svg/icon-hackers',
subhead: LocaleUtils.fluentFormat(locales, 'how-hackers-work-desc')
},
{
title: "What to do after a data breach",
stringId: LocaleUtils.fluentFormat(locales,"what-to-do-after-breach"),
class: "after-breach",
pathToPartial: "svg/icon-at",
subhead: LocaleUtils.fluentFormat(locales,"what-to-do-after-breach-desc"),
title: 'What to do after a data breach',
stringId: LocaleUtils.fluentFormat(locales, 'what-to-do-after-breach'),
class: 'after-breach',
pathToPartial: 'svg/icon-at',
subhead: LocaleUtils.fluentFormat(locales, 'what-to-do-after-breach-desc')
},
{
title: "How to create strong passwords",
stringId: LocaleUtils.fluentFormat(locales,"create-strong-passwords"),
class: "strong-passwords",
pathToPartial: "svg/icon-password",
subhead: LocaleUtils.fluentFormat(locales,"create-strong-passwords-desc"),
title: 'How to create strong passwords',
stringId: LocaleUtils.fluentFormat(locales, 'create-strong-passwords'),
class: 'strong-passwords',
pathToPartial: 'svg/icon-password',
subhead: LocaleUtils.fluentFormat(locales, 'create-strong-passwords-desc')
},
{
title: "Steps to take to protect your identity online",
stringId: LocaleUtils.fluentFormat(locales,"steps-to-protect"),
class: "steps-to-protect",
pathToPartial: "svg/icon-fingerprinters",
subhead: LocaleUtils.fluentFormat(locales,"steps-to-protect-desc"),
title: 'Steps to take to protect your identity online',
stringId: LocaleUtils.fluentFormat(locales, 'steps-to-protect'),
class: 'steps-to-protect',
pathToPartial: 'svg/icon-fingerprinters',
subhead: LocaleUtils.fluentFormat(locales, 'steps-to-protect-desc')
},
{
title: "5 myths about password managers",
stringId: LocaleUtils.fluentFormat(locales,"five-myths"),
class: "five-myths",
pathToPartial: "svg/icon-myths",
subhead: LocaleUtils.fluentFormat(locales,"five-myths-desc"),
title: '5 myths about password managers',
stringId: LocaleUtils.fluentFormat(locales, 'five-myths'),
class: 'five-myths',
pathToPartial: 'svg/icon-myths',
subhead: LocaleUtils.fluentFormat(locales, 'five-myths-desc')
},
{
title: "Take further steps to protect your identity",
stringId: LocaleUtils.fluentFormat(locales,"take-further-steps"),
class: "next-steps",
pathToPartial: "svg/icon-trackers",
subhead: LocaleUtils.fluentFormat(locales, "take-further-steps-desc"),
},
];
title: 'Take further steps to protect your identity',
stringId: LocaleUtils.fluentFormat(locales, 'take-further-steps'),
class: 'next-steps',
pathToPartial: 'svg/icon-trackers',
subhead: LocaleUtils.fluentFormat(locales, 'take-further-steps-desc')
}
]
return articleLinks;
return articleLinks
}
module.exports = {
articleLinks,
getArticleCopy,
getSecurityTipsIntro,
};
getSecurityTipsIntro
}

Просмотреть файл

@ -1,168 +1,162 @@
"use strict";
'use strict'
const AppConstants = require("./../app-constants");
const AppConstants = require('./../app-constants')
const { prettyDate, localize } = require("./hbs-helpers");
const { getAllPriorityDataClasses, getAllGenericRecommendations, getFourthPasswordRecommendation } = require("./recommendations");
const { getPromoStrings } = require("./product-promos");
const { prettyDate, localize } = require('./hbs-helpers')
const { getAllPriorityDataClasses, getAllGenericRecommendations, getFourthPasswordRecommendation } = require('./recommendations')
const { getPromoStrings } = require('./product-promos')
function addRecommendationUtmParams(cta) {
function addRecommendationUtmParams (cta) {
try {
const url = new URL(cta.ctaHref);
const url = new URL(cta.ctaHref)
if (url.host.match(/mozilla\.org|firefox\.com/) === null) {
return cta.ctaHref;
return cta.ctaHref
}
const utmParams = {
utm_source: "monitor.firefox.com",
utm_medium: "referral",
utm_source: 'monitor.firefox.com',
utm_medium: 'referral',
utm_content: cta.ctaAnalyticsId,
utm_campaign: "contextual-recommendations",
};
utm_campaign: 'contextual-recommendations'
}
for (const param in utmParams) {
url.searchParams.append(param, utmParams[param]);
url.searchParams.append(param, utmParams[param])
}
return url.href;
}
catch (e) {
return cta.ctaHref;
return url.href
} catch (e) {
return cta.ctaHref
}
}
function getBreachTitle(args) {
return args.data.root.featuredBreach.Title;
function getBreachTitle (args) {
return args.data.root.featuredBreach.Title
}
function getVars(args) {
const locales = args.data.root.req.supportedLocales;
const breach = args.data.root.featuredBreach;
const changePWLink = args.data.root.changePWLink;
const isUserBrowserFirefox = (/Firefox/i.test(args.data.root.req.headers["user-agent"]));
return { locales, breach, changePWLink, isUserBrowserFirefox };
function getVars (args) {
const locales = args.data.root.req.supportedLocales
const breach = args.data.root.featuredBreach
const changePWLink = args.data.root.changePWLink
const isUserBrowserFirefox = (/Firefox/i.test(args.data.root.req.headers['user-agent']))
return { locales, breach, changePWLink, isUserBrowserFirefox }
}
function getBreachCategory(breach) {
if (["Exactis", "Apollo", "YouveBeenScraped", "ElasticsearchSalesLeads", "Estonia", "MasterDeeds", "PDL"].includes(breach.Name)) {
return "data-aggregator-breach";
function getBreachCategory (breach) {
if (['Exactis', 'Apollo', 'YouveBeenScraped', 'ElasticsearchSalesLeads', 'Estonia', 'MasterDeeds', 'PDL'].includes(breach.Name)) {
return 'data-aggregator-breach'
}
if (breach.IsSensitive) {
return "sensitive-breach";
return 'sensitive-breach'
}
if (breach.Domain !== "") {
return "website-breach";
if (breach.Domain !== '') {
return 'website-breach'
}
return "data-aggregator-breach";
return 'data-aggregator-breach'
}
function getSortedDataClasses(locales, breach, isUserBrowserFirefox=false, isUserLocaleEnUs=false, isUserLocalEn=false, changePWLink=false) {
const priorityDataClasses = getAllPriorityDataClasses(isUserBrowserFirefox, isUserLocaleEnUs, changePWLink);
function getSortedDataClasses (locales, breach, isUserBrowserFirefox = false, isUserLocaleEnUs = false, isUserLocalEn = false, changePWLink = false) {
const priorityDataClasses = getAllPriorityDataClasses(isUserBrowserFirefox, isUserLocaleEnUs, changePWLink)
const sortedDataClasses = {
priority: [],
lowerPriority: [],
};
const dataClasses = breach.DataClasses;
dataClasses.forEach(dataClass => {
const dataType = localize(locales, dataClass);
if (priorityDataClasses[dataClass]) {
priorityDataClasses[dataClass]["dataType"] = dataType;
sortedDataClasses.priority.push(priorityDataClasses[dataClass]);
return;
}
sortedDataClasses.lowerPriority.push(dataType);
});
sortedDataClasses.priority.sort((a,b) => { return b.weight - a.weight; });
sortedDataClasses.lowerPriority = sortedDataClasses.lowerPriority.join(", ");
return sortedDataClasses;
}
function compareBreachDates(breach) {
const breachDate = new Date(breach.BreachDate);
const addedDate = new Date(breach.AddedDate);
const timeDiff = Math.abs(breachDate.getTime() - addedDate.getTime());
const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
if (dayDiff > 90) {
return true;
lowerPriority: []
}
return false;
const dataClasses = breach.DataClasses
dataClasses.forEach(dataClass => {
const dataType = localize(locales, dataClass)
if (priorityDataClasses[dataClass]) {
priorityDataClasses[dataClass].dataType = dataType
sortedDataClasses.priority.push(priorityDataClasses[dataClass])
return
}
sortedDataClasses.lowerPriority.push(dataType)
})
sortedDataClasses.priority.sort((a, b) => { return b.weight - a.weight })
sortedDataClasses.lowerPriority = sortedDataClasses.lowerPriority.join(', ')
return sortedDataClasses
}
function getGenericFillerRecs(locales, numberOfRecsNeeded) {
let genericRecommendations = getAllGenericRecommendations();
function compareBreachDates (breach) {
const breachDate = new Date(breach.BreachDate)
const addedDate = new Date(breach.AddedDate)
const timeDiff = Math.abs(breachDate.getTime() - addedDate.getTime())
const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24))
if (dayDiff > 90) {
return true
}
return false
}
function getGenericFillerRecs (locales, numberOfRecsNeeded) {
let genericRecommendations = getAllGenericRecommendations()
genericRecommendations = genericRecommendations
.slice(0, numberOfRecsNeeded); // Slice array down to number of needed recommendations
.slice(0, numberOfRecsNeeded) // Slice array down to number of needed recommendations
genericRecommendations.forEach(rec => {
for (const pieceOfCopy in rec.recommendationCopy) {
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy]);
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy])
}
});
return genericRecommendations;
})
return genericRecommendations
}
function getBreachDetail(args) {
const acceptsLanguages = args.data.root.req.acceptsLanguages();
const { locales, breach, changePWLink, isUserBrowserFirefox } = getVars(args);
const { sortedDataClasses, recommendations } = getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox, changePWLink);
const breachCategory = getBreachCategory(breach);
const breachExposedPasswords = breach.DataClasses.includes("passwords");
function getBreachDetail (args) {
const acceptsLanguages = args.data.root.req.acceptsLanguages()
const { locales, breach, changePWLink, isUserBrowserFirefox } = getVars(args)
const { sortedDataClasses, recommendations } = getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox, changePWLink)
const breachCategory = getBreachCategory(breach)
const breachExposedPasswords = breach.DataClasses.includes('passwords')
breach.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breach.LogoPath}`;
breach.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breach.LogoPath}`
const breachDetail = {
breach: breach,
breachExposedPasswords: breachExposedPasswords,
breach,
breachExposedPasswords,
overview: {
headline: localize(locales, "breach-overview-title"),
copy: localize(locales, "breach-overview-new", {
headline: localize(locales, 'breach-overview-title'),
copy: localize(locales, 'breach-overview-new', {
addedDate: `<span class='bold'>${prettyDate(breach.AddedDate, locales)}</span>`,
breachDate: `<span class='bold'>${prettyDate(breach.BreachDate, locales)}</span>`,
breachTitle: breach.Title,
}),
breachTitle: breach.Title
})
},
categoryId: breachCategory,
category: localize(locales, breachCategory),
changePWLink: changePWLink,
changePWLink,
dataClasses: {
headline: localize(locales, "what-data"),
dataTypes: sortedDataClasses,
headline: localize(locales, 'what-data'),
dataTypes: sortedDataClasses
},
recommendations: {
headline: breachExposedPasswords ? localize(locales, "rec-section-headline") : localize(locales, "rec-section-headline-no-pw"),
copy: breachExposedPasswords ? localize(locales, "rec-section-subhead") : localize(locales, "rec-section-subhead-no-pw"),
recommendationsList: recommendations,
},
};
headline: breachExposedPasswords ? localize(locales, 'rec-section-headline') : localize(locales, 'rec-section-headline-no-pw'),
copy: breachExposedPasswords ? localize(locales, 'rec-section-subhead') : localize(locales, 'rec-section-subhead-no-pw'),
recommendationsList: recommendations
}
}
// Add correct "What is a ... breach" copy.
switch (breachDetail.categoryId) {
case "data-aggregator-breach":
case 'data-aggregator-breach':
breachDetail.whatIsThisBreach = {
headline: localize(locales, "what-is-data-agg"),
copy: localize(locales, "what-is-data-agg-blurb"),
};
break;
case "sensitive-breach":
headline: localize(locales, 'what-is-data-agg'),
copy: localize(locales, 'what-is-data-agg-blurb')
}
break
case 'sensitive-breach':
breachDetail.whatIsThisBreach = {
headline: localize(locales, "sensitive-sites"),
copy: localize(locales, "sensitive-sites-copy"),
};
break;
headline: localize(locales, 'sensitive-sites'),
copy: localize(locales, 'sensitive-sites-copy')
}
break
default:
breachDetail.whatIsThisBreach = {
headline: localize(locales, "what-is-a-website-breach"),
copy: localize(locales, "website-breach-blurb"),
};
headline: localize(locales, 'what-is-a-website-breach'),
copy: localize(locales, 'website-breach-blurb')
}
}
// Compare the breach date to the breach added date
@ -170,94 +164,93 @@ function getBreachDetail(args) {
// message if necessary.
if (compareBreachDates(breach)) {
breachDetail.delayedReporting = {
headline: localize(locales, "delayed-reporting-headline"),
copy: localize(locales, "delayed-reporting-copy"),
};
headline: localize(locales, 'delayed-reporting-headline'),
copy: localize(locales, 'delayed-reporting-copy')
}
}
// Determine which product promo to show
breachDetail.promo = getPromoStrings(args);
breachDetail.promo = getPromoStrings(args)
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === "1");
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === '1')
if (BREACH_RESOLUTION_ENABLED && args.data.root.affectedEmails) {
const affectedEmails = args.data.root.affectedEmails;
const numAffectedEmails = affectedEmails.length;
const unresolvedAffectedEmails = [];
const affectedEmails = args.data.root.affectedEmails
const numAffectedEmails = affectedEmails.length
const unresolvedAffectedEmails = []
if (numAffectedEmails > 0) {
affectedEmails.forEach(email => {
if (!email.isResolved) {
unresolvedAffectedEmails.push(email);
unresolvedAffectedEmails.push(email)
}
});
// show top of page alert for any emails involved in this breach where the breach
// has not yet been marked as resolved.
// if all breaches have been resolved, show nothing
if (unresolvedAffectedEmails.length > 0) {
const affectedEmailNotification = unresolvedAffectedEmails.length > 1 ?
localize(locales, "resolve-top-notification-plural", { numAffectedEmails: numAffectedEmails }) :
localize(locales, "resolve-top-notification", { affectedEmail: unresolvedAffectedEmails[0].emailAddress });
})
// show top of page alert for any emails involved in this breach where the breach
// has not yet been marked as resolved.
// if all breaches have been resolved, show nothing
if (unresolvedAffectedEmails.length > 0) {
const affectedEmailNotification = unresolvedAffectedEmails.length > 1
? localize(locales, 'resolve-top-notification-plural', { numAffectedEmails })
: localize(locales, 'resolve-top-notification', { affectedEmail: unresolvedAffectedEmails[0].emailAddress })
breachDetail.affectedEmailNotification = formatNotificationLink(affectedEmailNotification);
}
breachDetail.affectedEmails = affectedEmails;
breachDetail.affectedEmailNotification = formatNotificationLink(affectedEmailNotification)
}
breachDetail.affectedEmails = affectedEmails
breachDetail.resolutionStrings = {
subhead: localize(locales, "marking-this-subhead"),
message: formatResolutionMessage(localize(locales, "marking-this-body")),
resolveButtonTitle: localize(locales, "mark-as-resolve-button"),
resolvedLabel: localize(locales, "marked-as-resolved-label"),
undoResolvedLabel: localize(locales, "undo-button"),
};
subhead: localize(locales, 'marking-this-subhead'),
message: formatResolutionMessage(localize(locales, 'marking-this-body')),
resolveButtonTitle: localize(locales, 'mark-as-resolve-button'),
resolvedLabel: localize(locales, 'marked-as-resolved-label'),
undoResolvedLabel: localize(locales, 'undo-button')
}
}
}
return args.fn(breachDetail);
return args.fn(breachDetail)
}
function formatResolutionMessage(message) {
return message.replace("<span>", "<span class='demi'>");
function formatResolutionMessage (message) {
return message.replace('<span>', "<span class='demi'>")
}
function formatNotificationLink(message) {
return message.replace("<a>", "<a class='what-to-do-next blue-link' href='#what-to-do-next' data-analytics-label='what-to-do-next'>");
function formatNotificationLink (message) {
return message.replace('<a>', "<a class='what-to-do-next blue-link' href='#what-to-do-next' data-analytics-label='what-to-do-next'>")
}
function getSortedDataClassesAndRecs (acceptsLanguages, locales, breach, isUserBrowserFirefox = false, changePWLink = false) {
const isUserLocaleEn = (acceptsLanguages[0].toLowerCase().startsWith('en'))
const isUserLocaleEnUs = (acceptsLanguages[0].toLowerCase() === 'en-us')
const sortedDataClasses = getSortedDataClasses(locales, breach, isUserBrowserFirefox, isUserLocaleEnUs, isUserLocaleEn, changePWLink)
function getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox=false, changePWLink=false) {
const isUserLocaleEn = (acceptsLanguages[0].toLowerCase().startsWith("en"));
const isUserLocaleEnUs = (acceptsLanguages[0].toLowerCase() === "en-us");
const sortedDataClasses = getSortedDataClasses(locales, breach, isUserBrowserFirefox, isUserLocaleEnUs, isUserLocaleEn, changePWLink);
let recommendations = [];
let recommendations = []
// Check each priority data class for a recommendation
// and push localized recommendations into new array.
sortedDataClasses.priority.forEach(dataClass => {
if (dataClass.recommendations) {
const recs = dataClass.recommendations;
const recs = dataClass.recommendations
recs.forEach(rec => {
for (const pieceOfCopy in rec.recommendationCopy) {
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy]);
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy])
}
recommendations.push(rec);
});
recommendations.push(rec)
})
}
});
})
// If the breach exposed passwords, push the fourth password recommendation
// to the end of the recommendations list regardless of list length.
if (breach.DataClasses.includes("passwords")) {
recommendations.push(getFourthPasswordRecommendation(locales));
if (breach.DataClasses.includes('passwords')) {
recommendations.push(getFourthPasswordRecommendation(locales))
}
// If there are fewer than four recommendations,
// backfill with generic recommendations.
const minimumNumberOfRecs = 4;
const minimumNumberOfRecs = 4
if (recommendations.length < minimumNumberOfRecs) {
const numberOfRecsNeeded = minimumNumberOfRecs - recommendations.length;
const genericRecs = getGenericFillerRecs(locales, numberOfRecsNeeded);
recommendations = recommendations.concat(genericRecs);
const numberOfRecsNeeded = minimumNumberOfRecs - recommendations.length
const genericRecs = getGenericFillerRecs(locales, numberOfRecsNeeded)
recommendations = recommendations.concat(genericRecs)
}
return {sortedDataClasses, recommendations};
return { sortedDataClasses, recommendations }
}
module.exports = {
@ -265,5 +258,5 @@ module.exports = {
getBreachDetail,
getBreachCategory,
getSortedDataClasses,
getBreachTitle,
};
getBreachTitle
}

Просмотреть файл

@ -1,115 +1,112 @@
"use strict";
'use strict'
const AppConstants = require("../app-constants");
const { resultsSummary } = require("../scan-results");
const { localize } = require("./hbs-helpers");
const AppConstants = require('../app-constants')
const { resultsSummary } = require('../scan-results')
const { localize } = require('./hbs-helpers')
function getBreachStats(args) {
const verifiedEmails = args.data.root.verifiedEmails;
const locales = args.data.root.req.supportedLocales;
function getBreachStats (args) {
const verifiedEmails = args.data.root.verifiedEmails
const locales = args.data.root.req.supportedLocales
const userBreachStats = {
breachStats: resultsSummary(verifiedEmails),
progressBar: "",
progressIntro: "",
};
progressBar: '',
progressIntro: ''
}
const breachStatBundle = userBreachStats.breachStats;
const totalEmailsStat = breachStatBundle.monitoredEmails;
const breachStatBundle = userBreachStats.breachStats
const totalEmailsStat = breachStatBundle.monitoredEmails
// Format "00 emails being monitored" callout
totalEmailsStat.subhead = localize(locales, "email-addresses-being-monitored", { emails: verifiedEmails.length });
totalEmailsStat.displayCount = breachStatBundle.monitoredEmails.count;
totalEmailsStat.subhead = localize(locales, 'email-addresses-being-monitored', { emails: verifiedEmails.length })
totalEmailsStat.displayCount = breachStatBundle.monitoredEmails.count
const breachesStat = breachStatBundle.numBreaches;
const passwordStat = breachStatBundle.passwords;
const breachesStat = breachStatBundle.numBreaches
const passwordStat = breachStatBundle.passwords
if (breachesStat.numResolved > 0) {
// If a user has resolved at least one breach:
// Change the password stat to show the number of password-exposing unresolved breaches.
const remainingExposedPasswords = passwordStat.count - passwordStat.numResolved;
passwordStat.subhead = localize(locales, "unresolved-passwords-exposed", { numPasswords: remainingExposedPasswords });
passwordStat.displayCount = remainingExposedPasswords;
const remainingExposedPasswords = passwordStat.count - passwordStat.numResolved
passwordStat.subhead = localize(locales, 'unresolved-passwords-exposed', { numPasswords: remainingExposedPasswords })
passwordStat.displayCount = remainingExposedPasswords
// Change the total number of breaches callout to show the total number of resolved breaches
breachesStat.subhead = localize(locales, "known-data-breaches-resolved", { numResolvedBreaches: breachesStat.numResolved });
breachesStat.displayCount = breachesStat.numResolved;
breachesStat.subhead = localize(locales, 'known-data-breaches-resolved', { numResolvedBreaches: breachesStat.numResolved })
breachesStat.displayCount = breachesStat.numResolved
} else {
passwordStat.subhead = localize(locales, 'passwords-exposed', { passwords: passwordStat.count })
passwordStat.displayCount = passwordStat.count
passwordStat.subhead = localize(locales, "passwords-exposed", { passwords: passwordStat.count });
passwordStat.displayCount = passwordStat.count;
breachesStat.subhead = localize(locales, "known-data-breaches-exposed", { breaches: breachesStat.count });
breachesStat.displayCount = breachesStat.count;
breachesStat.subhead = localize(locales, 'known-data-breaches-exposed', { breaches: breachesStat.count })
breachesStat.displayCount = breachesStat.count
}
// add progress bar strings
if (AppConstants.BREACH_RESOLUTION_ENABLED === "1") {
userBreachStats.progressBar = makeProgressBar(breachesStat, locales);
if (AppConstants.BREACH_RESOLUTION_ENABLED === '1') {
userBreachStats.progressBar = makeProgressBar(breachesStat, locales)
}
return args.fn(userBreachStats);
return args.fn(userBreachStats)
}
function formatProgressMessage(message) {
return message.replace("<span>", "<span class='demi'>");
function formatProgressMessage (message) {
return message.replace('<span>', "<span class='demi'>")
}
function getProgressMessage(locales, percentBreachesResolved) {
function getProgressMessage (locales, percentBreachesResolved) {
if (percentBreachesResolved <= 25) {
return formatProgressMessage(localize(locales, "progress-message-1"));
return formatProgressMessage(localize(locales, 'progress-message-1'))
}
if (percentBreachesResolved <= 50) {
return formatProgressMessage(localize(locales, "progress-message-2"));
return formatProgressMessage(localize(locales, 'progress-message-2'))
}
if (percentBreachesResolved <= 75) {
return formatProgressMessage(localize(locales, "progress-message-3"));
return formatProgressMessage(localize(locales, 'progress-message-3'))
}
return formatProgressMessage(localize(locales, "progress-message-4"));
return formatProgressMessage(localize(locales, 'progress-message-4'))
}
function makeProgressBar(userBreachTotals, locales) {
const numTotalBreaches = userBreachTotals.count;
const numResolvedBreaches = userBreachTotals.numResolved;
function makeProgressBar (userBreachTotals, locales) {
const numTotalBreaches = userBreachTotals.count
const numResolvedBreaches = userBreachTotals.numResolved
// Show nothing if there are no found breaches for any email
if (numTotalBreaches === 0) {
return null;
return null
}
// Show an introductory message about resolving breaches if the user
// has found breaches, but hasn't resolved any of them yet.
if (numResolvedBreaches === 0) {
return {
subhead: localize(locales, "progress-intro-subhead"),
progressMessage: localize(locales, "progress-intro-message"),
imageClassName: "breach-resolution-intro",
};
subhead: localize(locales, 'progress-intro-subhead'),
progressMessage: localize(locales, 'progress-intro-message'),
imageClassName: 'breach-resolution-intro'
}
}
let percentBreachesResolved = Math.floor(numResolvedBreaches / numTotalBreaches * 100);
percentBreachesResolved = percentBreachesResolved < 1 ? 1 : percentBreachesResolved;
let percentBreachesResolved = Math.floor(numResolvedBreaches / numTotalBreaches * 100)
percentBreachesResolved = percentBreachesResolved < 1 ? 1 : percentBreachesResolved
if (percentBreachesResolved === 100) {
return {
subhead: localize(locales, "progress-complete"),
progressMessage: formatProgressMessage(localize(locales, "progress-complete-message")),
imageClassName: "breach-resolution-complete",
};
subhead: localize(locales, 'progress-complete'),
progressMessage: formatProgressMessage(localize(locales, 'progress-complete-message')),
imageClassName: 'breach-resolution-complete'
}
}
// Show the progress bar if a user has resolved at least one breach
// and has others left to resolve.
return {
progressStatus: localize(locales, "progress-status", {
"numResolvedBreaches": numResolvedBreaches,
"numTotalBreaches": numTotalBreaches,
progressStatus: localize(locales, 'progress-status', {
numResolvedBreaches,
numTotalBreaches
}),
percentComplete: localize(locales, "progress-percent-complete", { "percentComplete": percentBreachesResolved}),
percentComplete: localize(locales, 'progress-percent-complete', { percentComplete: percentBreachesResolved }),
progressMessage: getProgressMessage(locales, percentBreachesResolved),
percentBreachesResolved: percentBreachesResolved,
};
percentBreachesResolved
}
}
module.exports = {
getBreachStats,
};
getBreachStats
}

Просмотреть файл

@ -1,142 +1,141 @@
"use strict";
'use strict'
const AppConstants = require("./../app-constants");
const { getSortedDataClasses } = require("./breach-detail");
const { prettyDate, localeString, localizedBreachDataClasses } = require("./hbs-helpers");
const { LocaleUtils } = require("./../locale-utils");
const { filterBreaches } = require("./../hibp");
const AppConstants = require('./../app-constants')
const { getSortedDataClasses } = require('./breach-detail')
const { prettyDate, localeString, localizedBreachDataClasses } = require('./hbs-helpers')
const { LocaleUtils } = require('./../locale-utils')
const { filterBreaches } = require('./../hibp')
function getLocalizedBreachCardStrings(locales) {
function getLocalizedBreachCardStrings (locales) {
return {
BreachAdded : LocaleUtils.fluentFormat(locales, "breach-added-label"),
CompromisedAccounts: LocaleUtils.fluentFormat(locales, "compromised-accounts"),
CompromisedData: LocaleUtils.fluentFormat(locales, "compromised-data"),
LatestBreachLink: LocaleUtils.fluentFormat(locales, "latest-breach-link"),
MoreInfoLink: LocaleUtils.fluentFormat(locales, "more-about-this-breach"),
ResolveThisBreachLink: LocaleUtils.fluentFormat(locales, "resolve-this-breach-link"),
};
BreachAdded: LocaleUtils.fluentFormat(locales, 'breach-added-label'),
CompromisedAccounts: LocaleUtils.fluentFormat(locales, 'compromised-accounts'),
CompromisedData: LocaleUtils.fluentFormat(locales, 'compromised-data'),
LatestBreachLink: LocaleUtils.fluentFormat(locales, 'latest-breach-link'),
MoreInfoLink: LocaleUtils.fluentFormat(locales, 'more-about-this-breach'),
ResolveThisBreachLink: LocaleUtils.fluentFormat(locales, 'resolve-this-breach-link')
}
}
function dataClassesforCards(breach, locales) {
const topTwoClasses = [];
const dataClasses = getSortedDataClasses(locales, breach);
function dataClassesforCards (breach, locales) {
const topTwoClasses = []
const dataClasses = getSortedDataClasses(locales, breach)
dataClasses.priority.forEach(dataType => {
topTwoClasses.push(dataType.dataType);
});
topTwoClasses.push(dataType.dataType)
})
if (topTwoClasses.length >= 2) {
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales);
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales)
}
topTwoClasses.concat(dataClasses.lowerPriority);
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales);
topTwoClasses.concat(dataClasses.lowerPriority)
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales)
}
function sortBreaches(breaches) {
breaches = breaches.sort((a,b) => {
const oldestBreach = new Date(a.AddedDate);
const newestBreach = new Date(b.AddedDate);
return newestBreach-oldestBreach;
});
return breaches;
function sortBreaches (breaches) {
breaches = breaches.sort((a, b) => {
const oldestBreach = new Date(a.AddedDate)
const newestBreach = new Date(b.AddedDate)
return newestBreach - oldestBreach
})
return breaches
}
function makeBreachCards(breaches, locales) {
const formattedBreaches = [];
const breachCardStrings = getLocalizedBreachCardStrings(locales);
breaches = JSON.parse(JSON.stringify(breaches));
function makeBreachCards (breaches, locales) {
const formattedBreaches = []
const breachCardStrings = getLocalizedBreachCardStrings(locales)
breaches = JSON.parse(JSON.stringify(breaches))
for (const breachCard of breaches) {
getLocalizedBreachValues(locales, breachCard);
breachCard.LocalizedBreachCardStrings = breachCardStrings; // "Compromised Data: , Compromised Accounts: ..."
breachCard.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breachCard.LogoPath}`;
formattedBreaches.push(breachCard);
getLocalizedBreachValues(locales, breachCard)
breachCard.LocalizedBreachCardStrings = breachCardStrings // "Compromised Data: , Compromised Accounts: ..."
breachCard.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breachCard.LogoPath}`
formattedBreaches.push(breachCard)
}
return formattedBreaches;
return formattedBreaches
}
function lastAddedBreach(options) {
const locales = options.data.root.req.supportedLocales;
let latestBreach = [options.data.root.latestBreach];
latestBreach = makeBreachCards(latestBreach, locales);
return latestBreach;
function lastAddedBreach (options) {
const locales = options.data.root.req.supportedLocales
let latestBreach = [options.data.root.latestBreach]
latestBreach = makeBreachCards(latestBreach, locales)
return latestBreach
}
function getFoundBreaches(args) {
const locales = args.data.root.req.supportedLocales;
let userBreaches = args.data.root.foundBreaches;
userBreaches = makeBreachCards(userBreaches, locales);
userBreaches.cardType = "two-up drop-shadow";
return userBreaches;
function getFoundBreaches (args) {
const locales = args.data.root.req.supportedLocales
let userBreaches = args.data.root.foundBreaches
userBreaches = makeBreachCards(userBreaches, locales)
userBreaches.cardType = 'two-up drop-shadow'
return userBreaches
}
function getLocalizedBreachValues(locales, breach) {
breach.AddedDate = prettyDate(breach.AddedDate, locales);
breach.PwnCount = localeString(breach.PwnCount,locales);
breach.DataClasses = dataClassesforCards(breach, locales);
return breach;
function getLocalizedBreachValues (locales, breach) {
breach.AddedDate = prettyDate(breach.AddedDate, locales)
breach.PwnCount = localeString(breach.PwnCount, locales)
breach.DataClasses = dataClassesforCards(breach, locales)
return breach
}
function getBreachArray(args) {
const locales = args.data.root.req.supportedLocales;
function getBreachArray (args) {
const locales = args.data.root.req.supportedLocales
let breaches = args.data.root.req.app.locals.breaches;
breaches = JSON.parse(JSON.stringify(breaches));
let breaches = args.data.root.req.app.locals.breaches
breaches = JSON.parse(JSON.stringify(breaches))
// should we consider filtering the breaches when the app loads
// since we aren't ever showing them now anyway?
breaches = filterBreaches(breaches);
breaches = sortBreaches(breaches);
breaches = breaches.filter(breach => !breach.IsSensitive);
breaches = filterBreaches(breaches)
breaches = sortBreaches(breaches)
breaches = breaches.filter(breach => !breach.IsSensitive)
breaches.forEach(breach => {
getLocalizedBreachValues(locales, breach);
delete(breach.Description);
delete(breach.IsVerified);
delete(breach.ModifiedDate);
delete(breach.IsFabricated);
delete(breach.Domain);
delete(breach.IsRetired);
delete(breach.IsSensitive);
delete(breach.IsSpamList);
delete(breach.BreachDate);
});
getLocalizedBreachValues(locales, breach)
delete (breach.Description)
delete (breach.IsVerified)
delete (breach.ModifiedDate)
delete (breach.IsFabricated)
delete (breach.Domain)
delete (breach.IsRetired)
delete (breach.IsSensitive)
delete (breach.IsSpamList)
delete (breach.BreachDate)
})
const allBreaches = {
LocalizedBreachCardStrings: getLocalizedBreachCardStrings(locales),
breaches: breaches,
};
return JSON.stringify(allBreaches);
breaches
}
return JSON.stringify(allBreaches)
}
function getBreachCardCta(breach, args) {
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === "1");
const templateData = args.data.root;
function getBreachCardCta (breach, args) {
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === '1')
const templateData = args.data.root
if (breach.latestBreach) {
return args.fn({
ctaTitle: breach.LocalizedBreachCardStrings.LatestBreachLink,
ctaAnalyticsLabel: "Latest Breach: See if you were in this breach",
});
ctaAnalyticsLabel: 'Latest Breach: See if you were in this breach'
})
}
if (BREACH_RESOLUTION_ENABLED && templateData.whichPartial === "dashboards/breaches-dash" && !breach.IsResolved) {
return args.fn({
ctaTitle: breach.LocalizedBreachCardStrings.ResolveThisBreachLink,
ctaAnalyticsLabel: "Breach Card: Resolve this breach",
});
if (BREACH_RESOLUTION_ENABLED && templateData.whichPartial === 'dashboards/breaches-dash' && !breach.IsResolved) {
return args.fn({
ctaTitle: breach.LocalizedBreachCardStrings.ResolveThisBreachLink,
ctaAnalyticsLabel: 'Breach Card: Resolve this breach'
})
}
return args.fn({
ctaTitle: breach.LocalizedBreachCardStrings.MoreInfoLink,
ctaAnalyticsLabel: "Breach Card: More about this breach",
});
ctaAnalyticsLabel: 'Breach Card: More about this breach'
})
}
module.exports = {
lastAddedBreach,
getFoundBreaches,
makeBreachCards,
getBreachArray,
getBreachCardCta,
};
getBreachCardCta
}

Просмотреть файл

@ -1,183 +1,181 @@
"use strict";
'use strict'
const { LocaleUtils } = require("./../locale-utils");
const { makeBreachCards } = require("./breaches");
const { hasUserSignedUpForRelay } = require("./../controllers/utils");
const { LocaleUtils } = require('./../locale-utils')
const { makeBreachCards } = require('./breaches')
const { hasUserSignedUpForRelay } = require('./../controllers/utils')
function enLocaleIsSupported(args) {
return args.data.root.req.headers["accept-language"].includes("en");
function enLocaleIsSupported (args) {
return args.data.root.req.headers['accept-language'].includes('en')
}
function userIsOnRelayWaitList(args) {
return hasUserSignedUpForRelay(args.data.root.req.user);
function userIsOnRelayWaitList (args) {
return hasUserSignedUpForRelay(args.data.root.req.user)
}
function getBreachesDashboard(args) {
const verifiedEmails = args.data.root.verifiedEmails;
const locales = args.data.root.req.supportedLocales;
let breachesFound = false;
function getBreachesDashboard (args) {
const verifiedEmails = args.data.root.verifiedEmails
const locales = args.data.root.req.supportedLocales
let breachesFound = false
// move emails with 0 breaches to the bottom of the page
verifiedEmails.sort((a, b) => {
if (
a.breaches.length === 0 && b.breaches.length > 0 ||
a.breaches.length === 0 && b.breaches.length > 0 ||
b.breaches.length === 0 && a.breaches.length > 0
) {
return b.breaches.length - a.breaches.length;
) {
return b.breaches.length - a.breaches.length
}
return 0;
});
return 0
})
verifiedEmails.forEach(email => {
const breachCards = makeBreachCards(email.breaches, locales);
const breachCards = makeBreachCards(email.breaches, locales)
if (!breachesFound && breachCards.length > 0) {
breachesFound = true;
breachesFound = true
}
email.numBreaches = breachCards.length;
email.numResolvedBreaches = 0;
email.numUnresolvedBreaches = 0;
email.numBreaches = breachCards.length
email.numResolvedBreaches = 0
email.numUnresolvedBreaches = 0
// Get the number of resolved breaches for email
email.breaches.forEach(breach => {
if (breach.IsResolved) {
email.numResolvedBreaches++;
email.numResolvedBreaches++
}
});
})
// Move resolved breaches to the end of breach list
if (email.numResolvedBreaches > 0) {
breachCards.sort((a,b) => {
breachCards.sort((a, b) => {
if (a.IsResolved && !b.IsResolved) {
return 1;
return 1
}
if (!a.IsResolved && b.IsResolved) {
return -1;
return -1
}
});
})
}
delete email.breaches;
email.numUnresolvedBreaches = email.numBreaches - email.numResolvedBreaches;
email.foundBreaches = {};
delete email.breaches
email.numUnresolvedBreaches = email.numBreaches - email.numResolvedBreaches
email.foundBreaches = {}
// If there are more than four unresolved breaches, show only the first four by default.
if (email.numUnresolvedBreaches > 4) {
email.foundBreaches.breachesShownByDefault = breachCards.slice(0, 4);
email.foundBreaches.remainingBreaches = breachCards.slice(4, breachCards.length);
email.foundBreaches.breachesShownByDefault = breachCards.slice(0, 4)
email.foundBreaches.remainingBreaches = breachCards.slice(4, breachCards.length)
} else {
email.foundBreaches.breachesShownByDefault = breachCards;
email.foundBreaches.breachesShownByDefault = breachCards
}
});
})
const emailCards = {
verifiedEmails: verifiedEmails,
breachesFound: breachesFound,
};
return args.fn(emailCards);
}
function welcomeMessage(args) {
const locales = args.data.root.req.supportedLocales;
const userEmail = args.data.root.req.session.user.fxa_profile_json.email;
const newUser = args.data.root.req.session.newUser;
if (newUser) {
return LocaleUtils.fluentFormat(locales, "welcome-user", { userName: userEmail });
verifiedEmails,
breachesFound
}
return LocaleUtils.fluentFormat(locales, "welcome-back", { userName: userEmail});
return args.fn(emailCards)
}
function makeEmailAddedToSubscriptionString(email, args) {
const locales = args.data.root.req.supportedLocales;
const nestedEmail = `<span class="bold">${email}</span>`;
return LocaleUtils.fluentFormat(locales, "email-added-to-subscription", { email: nestedEmail });
function welcomeMessage (args) {
const locales = args.data.root.req.supportedLocales
const userEmail = args.data.root.req.session.user.fxa_profile_json.email
const newUser = args.data.root.req.session.newUser
if (newUser) {
return LocaleUtils.fluentFormat(locales, 'welcome-user', { userName: userEmail })
}
return LocaleUtils.fluentFormat(locales, 'welcome-back', { userName: userEmail })
}
function makeEmailVerifiedString(args) {
const locales = args.data.root.req.supportedLocales;
let nestedSignInLink = LocaleUtils.fluentFormat(locales, "sign-in-nested", {});
nestedSignInLink = `<a class="text-link bold blue-link" href="/oauth/init">${nestedSignInLink}</a>`;
return LocaleUtils.fluentFormat(locales, "email-verified-view-dashboard", { nestedSignInLink: nestedSignInLink});
function makeEmailAddedToSubscriptionString (email, args) {
const locales = args.data.root.req.supportedLocales
const nestedEmail = `<span class="bold">${email}</span>`
return LocaleUtils.fluentFormat(locales, 'email-added-to-subscription', { email: nestedEmail })
}
function makeEmailVerifiedString (args) {
const locales = args.data.root.req.supportedLocales
let nestedSignInLink = LocaleUtils.fluentFormat(locales, 'sign-in-nested', {})
nestedSignInLink = `<a class="text-link bold blue-link" href="/oauth/init">${nestedSignInLink}</a>`
function getUserPreferences(args) {
const csrfToken = args.data.root.csrfToken;
const unverifiedEmails = args.data.root.unverifiedEmails;
const verifiedEmails = args.data.root.verifiedEmails;
const sessionUser = args.data.root.req.session.user;
const communicationOption = (sessionUser.all_emails_to_primary) ? 1 : 0;
return LocaleUtils.fluentFormat(locales, 'email-verified-view-dashboard', { nestedSignInLink })
}
const locales = args.data.root.req.supportedLocales;
args.data.root.preferences = true;
function getUserPreferences (args) {
const csrfToken = args.data.root.csrfToken
const unverifiedEmails = args.data.root.unverifiedEmails
const verifiedEmails = args.data.root.verifiedEmails
const sessionUser = args.data.root.req.session.user
const communicationOption = (sessionUser.all_emails_to_primary) ? 1 : 0
const locales = args.data.root.req.supportedLocales
args.data.root.preferences = true
verifiedEmails.forEach(email => {
email.numBreaches = email.breaches.length;
delete email.breaches;
});
email.numBreaches = email.breaches.length
delete email.breaches
})
const primaryEmail = verifiedEmails.shift();
const primaryEmail = verifiedEmails.shift()
const emailAddresses = {
primary: {
subhead: LocaleUtils.fluentFormat(locales, "fxa-primary-email"),
className: "fxa-primary-email",
email_addresses: [ primaryEmail ], // put in array for template looping
subhead: LocaleUtils.fluentFormat(locales, 'fxa-primary-email'),
className: 'fxa-primary-email',
email_addresses: [primaryEmail] // put in array for template looping
},
secondary: {
subhead: LocaleUtils.fluentFormat(locales, "other-monitored-emails"),
className: "other-monitored-emails",
email_addresses: verifiedEmails,
subhead: LocaleUtils.fluentFormat(locales, 'other-monitored-emails'),
className: 'other-monitored-emails',
email_addresses: verifiedEmails
},
unverified: {
subhead: LocaleUtils.fluentFormat(locales, "email-verification-required"),
className: "email-verification-required",
email_addresses: unverifiedEmails,
subhead: LocaleUtils.fluentFormat(locales, 'email-verification-required'),
className: 'email-verification-required',
email_addresses: unverifiedEmails
},
total: [ primaryEmail ].length + verifiedEmails.length + unverifiedEmails.length,
};
total: [primaryEmail].length + verifiedEmails.length + unverifiedEmails.length
}
const communicationOptions = [
{
optionDescription: "Send breach alerts to the affected email address.",
labelString: LocaleUtils.fluentFormat(locales, "to-affected-email"),
optionId: "0",
optionChecked: (communicationOption === 0) ? "checked" : "",
optionDescription: 'Send breach alerts to the affected email address.',
labelString: LocaleUtils.fluentFormat(locales, 'to-affected-email'),
optionId: '0',
optionChecked: (communicationOption === 0) ? 'checked' : ''
},
{
optionDescription: "Send all breach alerts to subscriber's primary email address.",
labelString: LocaleUtils.fluentFormat(locales, "comm-opt-1", {primaryEmail: `<span class="bold">${primaryEmail.email}</span>`}),
optionId: "1",
optionChecked: (communicationOption === 1) ? "checked" : "",
},
];
labelString: LocaleUtils.fluentFormat(locales, 'comm-opt-1', { primaryEmail: `<span class="bold">${primaryEmail.email}</span>` }),
optionId: '1',
optionChecked: (communicationOption === 1) ? 'checked' : ''
}
]
const user = {
primaryEmail: primaryEmail.email,
emails : emailAddresses,
communicationOptions: communicationOptions,
csrfToken: csrfToken,
};
return args.fn(user);
emails: emailAddresses,
communicationOptions,
csrfToken
}
return args.fn(user)
}
function getLastAddedEmailStrings(args) {
const locales = args.data.root.req.supportedLocales;
const lastAddedEmail = args.data.root.lastAddedEmail;
const lastAddedEmailSpan = `<span class="bold">${lastAddedEmail}</span>`;
function getLastAddedEmailStrings (args) {
const locales = args.data.root.req.supportedLocales
const lastAddedEmail = args.data.root.lastAddedEmail
const lastAddedEmailSpan = `<span class="bold">${lastAddedEmail}</span>`
const preferencesLinkString = LocaleUtils.fluentFormat(locales, "preferences");
const preferencesLink = `<a class="demi text-link" href="/user/preferences">${preferencesLinkString}</a>`;
const preferencesLinkString = LocaleUtils.fluentFormat(locales, 'preferences')
const preferencesLink = `<a class="demi text-link" href="/user/preferences">${preferencesLinkString}</a>`
const lastAddedEmailStrings = [
LocaleUtils.fluentFormat(locales, "verify-the-link", { userEmail: lastAddedEmailSpan }),
LocaleUtils.fluentFormat(locales, "manage-all-emails", { preferencesLink }),
];
return lastAddedEmailStrings;
LocaleUtils.fluentFormat(locales, 'verify-the-link', { userEmail: lastAddedEmailSpan }),
LocaleUtils.fluentFormat(locales, 'manage-all-emails', { preferencesLink })
]
return lastAddedEmailStrings
}
module.exports = {
@ -188,5 +186,5 @@ module.exports = {
makeEmailVerifiedString,
makeEmailAddedToSubscriptionString,
enLocaleIsSupported,
userIsOnRelayWaitList,
};
userIsOnRelayWaitList
}

Просмотреть файл

@ -1,311 +1,299 @@
"use strict";
'use strict'
const { URL } = require("url");
const { URL } = require('url')
const { LocaleUtils } = require("./../locale-utils");
const { LocaleUtils } = require('./../locale-utils')
const { makeBreachCards } = require("./breaches");
const { prettyDate, vpnPromoBlocked } = require("./hbs-helpers");
const { makeBreachCards } = require('./breaches')
const { prettyDate, vpnPromoBlocked } = require('./hbs-helpers')
function emailBreachStats(args) {
const locales = args.data.root.supportedLocales;
const userBreaches = args.data.root.unsafeBreachesForEmail;
let numPasswordsExposed = 0;
function emailBreachStats (args) {
const locales = args.data.root.supportedLocales
const userBreaches = args.data.root.unsafeBreachesForEmail
let numPasswordsExposed = 0
userBreaches.forEach(breach => {
if (breach.DataClasses.includes("passwords")) {
numPasswordsExposed++;
if (breach.DataClasses.includes('passwords')) {
numPasswordsExposed++
}
});
})
const emailBreachStats = {
numBreaches: {
statNumber: userBreaches.length,
statTitle: LocaleUtils.fluentFormat(locales, "known-data-breaches-exposed", { breaches: userBreaches.length }),
statTitle: LocaleUtils.fluentFormat(locales, 'known-data-breaches-exposed', { breaches: userBreaches.length })
},
numPasswords: {
statNumber: numPasswordsExposed,
statTitle: LocaleUtils.fluentFormat(locales, "passwords-exposed", { passwords: numPasswordsExposed }),
},
};
return emailBreachStats;
}
function getPreFxaUtmParams(serverUrl, content, userEmail) {
const url = new URL(`${serverUrl}/oauth/init`);
const utmParams = {
utm_source: "fx-monitor",
utm_medium: "fx-monitor-email",
utm_content: content,
utm_campaign: "pre-fxa-subscribers",
email: userEmail,
};
for (const param in utmParams) {
url.searchParams.append(param, utmParams[param]);
statTitle: LocaleUtils.fluentFormat(locales, 'passwords-exposed', { passwords: numPasswordsExposed })
}
}
return url;
return emailBreachStats
}
function getPreFxaTouts(args) {
const locales = args.data.root.supportedLocales;
const serverUrl = args.data.root.SERVER_URL;
const userEmail = args.data.root.email;
function getPreFxaUtmParams (serverUrl, content, userEmail) {
const url = new URL(`${serverUrl}/oauth/init`)
const utmParams = {
utm_source: 'fx-monitor',
utm_medium: 'fx-monitor-email',
utm_content: content,
utm_campaign: 'pre-fxa-subscribers',
email: userEmail
}
for (const param in utmParams) {
url.searchParams.append(param, utmParams[param])
}
return url
}
function getPreFxaTouts (args) {
const locales = args.data.root.supportedLocales
const serverUrl = args.data.root.SERVER_URL
const userEmail = args.data.root.email
const fxaTouts = [
{
imgSrc: `${serverUrl}/img/email_images/pictogram-alert.png`,
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-1"),
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-1"),
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-1'),
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-1')
},
{
imgSrc: `${serverUrl}/img/email_images/pictogram-advice.png`,
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-2"),
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-2"),
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-2'),
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-2')
},
{
imgSrc: `${serverUrl}/img/email_images/pictogram-email.png`,
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-3"),
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-3"),
},
];
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-3'),
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-3')
}
]
// replace placeholder anchor tag markup in first tout to make link
// add UTM params which are passed to FxA for account creation
const fxaTout1 = fxaTouts[0].paragraph;
const url = getPreFxaUtmParams(serverUrl, "create-account-link", userEmail);
const fxaTout1 = fxaTouts[0].paragraph
const url = getPreFxaUtmParams(serverUrl, 'create-account-link', userEmail)
if ((/<a>/).test(fxaTout1) && (/<\/a>/).test(fxaTout1)) {
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 300; font-size: 15px; text-decoration: none;">`;
fxaTouts[0].paragraph = fxaTout1.replace("<a>", openingAnchorTag);
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 300; font-size: 15px; text-decoration: none;">`
fxaTouts[0].paragraph = fxaTout1.replace('<a>', openingAnchorTag)
}
return fxaTouts;
return fxaTouts
}
function getUnsafeBreachesForEmailReport(args) {
const locales = args.data.root.supportedLocales;
const foundBreaches = JSON.parse(JSON.stringify(args.data.root.unsafeBreachesForEmail));
function getUnsafeBreachesForEmailReport (args) {
const locales = args.data.root.supportedLocales
const foundBreaches = JSON.parse(JSON.stringify(args.data.root.unsafeBreachesForEmail))
if (foundBreaches.length > 4) {
foundBreaches.length = 4;
foundBreaches.length = 4
}
return makeBreachCards(foundBreaches, locales);
return makeBreachCards(foundBreaches, locales)
}
function boldVioletText(breachedEmail, addBlockDisplayToEmail = false) {
let optionalDisplayProperty = "";
function boldVioletText (breachedEmail, addBlockDisplayToEmail = false) {
let optionalDisplayProperty = ''
if (addBlockDisplayToEmail) {
optionalDisplayProperty = "display: block;";
optionalDisplayProperty = 'display: block;'
}
// garble email address so that email clients won't turn it into a link
breachedEmail = breachedEmail.replace(/([@.:])/g, "<span>$1</span>");
return `<span class="rec-email text-bold" style=" ${optionalDisplayProperty} font-weight: 700; color: #9059ff; font-family: sans-serif; text-decoration: none;"> ${breachedEmail}</span>`;
breachedEmail = breachedEmail.replace(/([@.:])/g, '<span>$1</span>')
return `<span class="rec-email text-bold" style=" ${optionalDisplayProperty} font-weight: 700; color: #9059ff; font-family: sans-serif; text-decoration: none;"> ${breachedEmail}</span>`
}
function getEmailHeader (args) {
const locales = args.data.root.supportedLocales
const emailType = args.data.root.whichPartial
const breachedEmail = args.data.root.breachedEmail
function getEmailHeader(args) {
const locales = args.data.root.supportedLocales;
const emailType = args.data.root.whichPartial;
const breachedEmail = args.data.root.breachedEmail;
if (emailType === "email_partials/email_verify") {
return LocaleUtils.fluentFormat(locales, "email-link-expires");
if (emailType === 'email_partials/email_verify') {
return LocaleUtils.fluentFormat(locales, 'email-link-expires')
}
if (emailType === "email_partials/pre-fxa") {
return LocaleUtils.fluentFormat(locales, "pre-fxa-headline");
if (emailType === 'email_partials/pre-fxa') {
return LocaleUtils.fluentFormat(locales, 'pre-fxa-headline')
}
if (args.data.root.breachAlert) {
return LocaleUtils.fluentFormat(locales, "email-alert-hl", { userEmail: boldVioletText(breachedEmail, true) });
return LocaleUtils.fluentFormat(locales, 'email-alert-hl', { userEmail: boldVioletText(breachedEmail, true) })
}
const userBreaches = args.data.root.unsafeBreachesForEmail;
const userBreaches = args.data.root.unsafeBreachesForEmail
if (userBreaches.length === 0) {
return LocaleUtils.fluentFormat(locales, "email-no-breaches-hl", { userEmail: boldVioletText(breachedEmail, true) });
return LocaleUtils.fluentFormat(locales, 'email-no-breaches-hl', { userEmail: boldVioletText(breachedEmail, true) })
}
return LocaleUtils.fluentFormat(locales, "email-found-breaches-hl");
return LocaleUtils.fluentFormat(locales, 'email-found-breaches-hl')
}
function makeFaqLink(target, campaign) {
const url = new URL(`https://support.mozilla.org/kb/firefox-monitor-faq${target}`);
function makeFaqLink (target, campaign) {
const url = new URL(`https://support.mozilla.org/kb/firefox-monitor-faq${target}`)
const utmParameters = {
utm_source: "fx-monitor",
utm_medium: "email",
utm_campaign: campaign,
};
utm_source: 'fx-monitor',
utm_medium: 'email',
utm_campaign: campaign
}
for (const param in utmParameters) {
url.searchParams.append(param, utmParameters[param]);
url.searchParams.append(param, utmParameters[param])
}
return url;
return url
}
function makePreFxaSubscriberMessage(args) {
const serverUrl = args.data.root.SERVER_URL;
const locales = args.data.root.supportedLocales;
const url = new URL(`${serverUrl}/#fx-account-features`);
function makePreFxaSubscriberMessage (args) {
const serverUrl = args.data.root.SERVER_URL
const locales = args.data.root.supportedLocales
const url = new URL(`${serverUrl}/#fx-account-features`)
const utmParameters = {
utm_source: "fx-monitor",
utm_medium: "email",
utm_content: "breach-alert",
utm_campaign: "pre-fxa-subscribers",
};
for (const param in utmParameters) {
url.searchParams.append(param, utmParameters[param]);
utm_source: 'fx-monitor',
utm_medium: 'email',
utm_content: 'breach-alert',
utm_campaign: 'pre-fxa-subscribers'
}
let preFxaMessage = LocaleUtils.fluentFormat(locales, "pre-fxa-message");
for (const param in utmParameters) {
url.searchParams.append(param, utmParameters[param])
}
let preFxaMessage = LocaleUtils.fluentFormat(locales, 'pre-fxa-message')
if ((/<a>/).test(preFxaMessage) && (/<\/a>/).test(preFxaMessage)) {
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 400; font-size: 16px; text-decoration: none;">`;
preFxaMessage = preFxaMessage.replace("<a>", openingAnchorTag);
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 400; font-size: 16px; text-decoration: none;">`
preFxaMessage = preFxaMessage.replace('<a>', openingAnchorTag)
}
return preFxaMessage;
return preFxaMessage
}
function getBreachAlertFaqs(args) {
const supportedLocales = args.data.root.supportedLocales;
function getBreachAlertFaqs (args) {
const supportedLocales = args.data.root.supportedLocales
const faqs = [
{
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-1", args),
"stringDescription": "I dont recognize one of these companies or websites. Why am I in this breach?",
"href": makeFaqLink("#w_i-donaot-recognize-this-company-or-website-why-am-i-receiving-notifications-about-this-breach", "faq1"),
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-1', args),
stringDescription: 'I dont recognize one of these companies or websites. Why am I in this breach?',
href: makeFaqLink('#w_i-donaot-recognize-this-company-or-website-why-am-i-receiving-notifications-about-this-breach', 'faq1')
},
{
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-2", args),
"stringDescription": "Do I need to do anything if a breach happened years ago or this is an old account?",
"href": makeFaqLink("#w_do-i-need-to-do-anything-if-a-breach-happened-years-ago-or-in-an-old-account", "faq2"),
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-2', args),
stringDescription: 'Do I need to do anything if a breach happened years ago or this is an old account?',
href: makeFaqLink('#w_do-i-need-to-do-anything-if-a-breach-happened-years-ago-or-in-an-old-account', 'faq2')
},
{
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-3", args),
"stringDescription": "I just found out Im in a data breach. What do I do next?",
"href": makeFaqLink("#w_i-just-found-out-im-in-a-data-breach-what-do-i-do-next", "faq3"),
},
];
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-3', args),
stringDescription: 'I just found out Im in a data breach. What do I do next?',
href: makeFaqLink('#w_i-just-found-out-im-in-a-data-breach-what-do-i-do-next', 'faq3')
}
]
if (args.data.root.breachAlert && args.data.root.breachAlert.IsSensitive) {
faqs.push({
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-4", args),
"stringDescription": "How does Firefox Monitor treat sensitive sites?",
"href": makeFaqLink("#w_how-does-firefox-monitor-treat-sensitive-sites", "faq4"),
});
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-4', args),
stringDescription: 'How does Firefox Monitor treat sensitive sites?',
href: makeFaqLink('#w_how-does-firefox-monitor-treat-sensitive-sites', 'faq4')
})
}
const functionedFaqs = faqs.map(faq => args.fn(faq));
return "".concat(...functionedFaqs);
const functionedFaqs = faqs.map(faq => args.fn(faq))
return ''.concat(...functionedFaqs)
}
function getReportHeader(args) {
const locales = args.data.root.supportedLocales;
function getReportHeader (args) {
const locales = args.data.root.supportedLocales
const reportHeader = {
date: prettyDate(new Date(), locales),
strings: {
"email": "email-address",
"reportDate": "report-date",
"fxmReport": "firefox-monitor-report",
},
};
email: 'email-address',
reportDate: 'report-date',
fxmReport: 'firefox-monitor-report'
}
}
for (const stringId in reportHeader.strings) {
const newString = LocaleUtils.fluentFormat(locales, reportHeader.strings[stringId]);
reportHeader.strings[stringId] = newString;
const newString = LocaleUtils.fluentFormat(locales, reportHeader.strings[stringId])
reportHeader.strings[stringId] = newString
}
return args.fn(reportHeader);
return args.fn(reportHeader)
}
function getEmailFooterCopy (args) {
const locales = args.data.root.supportedLocales
function getEmailFooterCopy(args) {
const locales = args.data.root.supportedLocales;
let faqLink = LocaleUtils.fluentFormat(locales, 'frequently-asked-questions')
faqLink = `<a href="https://support.mozilla.org/kb/firefox-monitor-faq">${faqLink}</a>`
let faqLink = LocaleUtils.fluentFormat(locales, "frequently-asked-questions");
faqLink = `<a href="https://support.mozilla.org/kb/firefox-monitor-faq">${faqLink}</a>`;
if (args.data.root.whichPartial === "email_partials/email_verify") {
return LocaleUtils.fluentFormat(locales, "email-verify-footer-copy", { faqLink });
if (args.data.root.whichPartial === 'email_partials/email_verify') {
return LocaleUtils.fluentFormat(locales, 'email-verify-footer-copy', { faqLink })
}
const unsubUrl = args.data.root.unsubscribeUrl;
const unsubLinkText = LocaleUtils.fluentFormat(locales, "email-unsub-link");
const unsubLink = `<a href="${unsubUrl}">${unsubLinkText}</a>`;
const unsubUrl = args.data.root.unsubscribeUrl
const unsubLinkText = LocaleUtils.fluentFormat(locales, 'email-unsub-link')
const unsubLink = `<a href="${unsubUrl}">${unsubLinkText}</a>`
const localizedFooterCopy = LocaleUtils.fluentFormat(locales, "email-footer-blurb", {
unsubLink: unsubLink,
faqLink: faqLink,
});
const localizedFooterCopy = LocaleUtils.fluentFormat(locales, 'email-footer-blurb', {
unsubLink,
faqLink
})
return localizedFooterCopy;
return localizedFooterCopy
}
function getEmailCTA (args) {
const locales = args.data.root.supportedLocales
const emailType = args.data.root.whichPartial
function getEmailCTA(args) {
const locales = args.data.root.supportedLocales;
const emailType = args.data.root.whichPartial;
if (emailType === "email_partials/email_verify") {
return LocaleUtils.fluentFormat(locales, "verify-email-cta");
if (emailType === 'email_partials/email_verify') {
return LocaleUtils.fluentFormat(locales, 'verify-email-cta')
}
return LocaleUtils.fluentFormat(locales, "go-to-dashboard-link");
return LocaleUtils.fluentFormat(locales, 'go-to-dashboard-link')
}
function getBreachSummaryHeadline(args) {
const locales = args.data.root.supportedLocales;
const breachedEmail = args.data.root.breachedEmail;
return LocaleUtils.fluentFormat(locales, "email-breach-summary-for-email", { userEmail: boldVioletText(breachedEmail) });
function getBreachSummaryHeadline (args) {
const locales = args.data.root.supportedLocales
const breachedEmail = args.data.root.breachedEmail
return LocaleUtils.fluentFormat(locales, 'email-breach-summary-for-email', { userEmail: boldVioletText(breachedEmail) })
}
function getBreachAlert(args) {
const locales = args.data.root.supportedLocales;
const breachAlert = [args.data.root.breachAlert];
const breachAlertCard = makeBreachCards(breachAlert, locales);
return args.fn(breachAlertCard[0]);
function getBreachAlert (args) {
const locales = args.data.root.supportedLocales
const breachAlert = [args.data.root.breachAlert]
const breachAlertCard = makeBreachCards(breachAlert, locales)
return args.fn(breachAlertCard[0])
}
// Show FAQs if the email type is a report with breaches, or a breach alert.
function showFaqs(args) {
if (args.data.root.whichPartial === "email_partials/email_verify") {
return;
function showFaqs (args) {
if (args.data.root.whichPartial === 'email_partials/email_verify') {
return
}
if (args.data.root.breachAlert || (args.data.root.unsafeBreachesForEmail && args.data.root.unsafeBreachesForEmail.length > 0)) {
return args.fn();
return args.fn()
}
}
function ifPreFxaSubscriber(args) {
function ifPreFxaSubscriber (args) {
if (args.data.root.preFxaSubscriber) {
return args.fn();
return args.fn()
}
return;
}
function getServerUrlForNestedEmailPartial(args) {
return args.data.root.SERVER_URL;
function getServerUrlForNestedEmailPartial (args) {
return args.data.root.SERVER_URL
}
function showProducts(args) {
const { whichPartial, breachAlert } = args.data.root;
function showProducts (args) {
const { whichPartial, breachAlert } = args.data.root
switch (true) {
case whichPartial === "email_partials/email_verify":
case whichPartial === 'email_partials/email_verify':
case vpnPromoBlocked(args):
return; // don't show products partial for the cases above
return // don't show products partial for the cases above
}
return args.fn({
strings: {
campaign: breachAlert ? `monitor-alert-emails&utm_content=${breachAlert.Name}` : "report",
},
});
campaign: breachAlert ? `monitor-alert-emails&utm_content=${breachAlert.Name}` : 'report'
}
})
}
module.exports = {
@ -324,5 +312,5 @@ module.exports = {
ifPreFxaSubscriber,
makePreFxaSubscriberMessage,
showFaqs,
showProducts,
};
showProducts
}

Просмотреть файл

@ -1,71 +1,71 @@
"use strict";
'use strict'
const { getStrings } = require("./hbs-helpers");
const { LocaleUtils } = require("./../locale-utils");
const { getStrings } = require('./hbs-helpers')
const { LocaleUtils } = require('./../locale-utils')
function getFooterLinks(args) {
const locales = args.data.root.req.supportedLocales;
function getFooterLinks (args) {
const locales = args.data.root.req.supportedLocales
const footerLinks = [
{
title: "About Firefox Monitor",
stringId: "about-firefox-monitor",
href: "/about",
title: 'About Firefox Monitor',
stringId: 'about-firefox-monitor',
href: '/about'
},
{
title: "Frequently Asked Questions",
stringId: "frequently-asked-questions",
href: "https://support.mozilla.org/kb/firefox-monitor-faq",
title: 'Frequently Asked Questions',
stringId: 'frequently-asked-questions',
href: 'https://support.mozilla.org/kb/firefox-monitor-faq'
},
{
title: "Terms & Privacy",
stringId: "terms-and-privacy",
href: "https://www.mozilla.org/privacy/firefox-monitor/?utm_campaign=fx_monitor_downloads&utm_content=site-footer-link&utm_medium=referral&utm_source=monitor.firefox.com",
title: 'Terms & Privacy',
stringId: 'terms-and-privacy',
href: 'https://www.mozilla.org/privacy/firefox-monitor/?utm_campaign=fx_monitor_downloads&utm_content=site-footer-link&utm_medium=referral&utm_source=monitor.firefox.com'
},
{
title: "GitHub",
stringId: "GitHub-link-title",
href: "https://github.com/mozilla/blurts-server",
},
];
title: 'GitHub',
stringId: 'GitHub-link-title',
href: 'https://github.com/mozilla/blurts-server'
}
]
return getStrings(footerLinks, locales);
return getStrings(footerLinks, locales)
}
function getAboutPageStrings(args) {
const locales = args.data.root.req.supportedLocales;
function getAboutPageStrings (args) {
const locales = args.data.root.req.supportedLocales
const aboutPageStrings = [
{
headline:"how-fxm-1-headline",
subhead: "how-fxm-1-blurb",
localizedCta: LocaleUtils.fluentFormat(locales, "scan-submit"),
href: "/",
eventCategory: "About Page: Search Your Email",
headline: 'how-fxm-1-headline',
subhead: 'how-fxm-1-blurb',
localizedCta: LocaleUtils.fluentFormat(locales, 'scan-submit'),
href: '/',
eventCategory: 'About Page: Search Your Email'
},
{
headline:"how-fxm-2-headline",
subhead: "how-fxm-2-blurb",
ctaId: "signUp",
localizedCta: LocaleUtils.fluentFormat(locales, "sign-up-for-alerts"),
headline: 'how-fxm-2-headline',
subhead: 'how-fxm-2-blurb',
ctaId: 'signUp',
localizedCta: LocaleUtils.fluentFormat(locales, 'sign-up-for-alerts')
},
{
headline:"how-fxm-3-headline",
subhead: "how-fxm-3-blurb",
localizedCta: LocaleUtils.fluentFormat(locales, "download-firefox-banner-button"),
href: "https://www.mozilla.org/firefox",
eventCategory: "About Page: Download Firefox",
download: "download",
},
];
headline: 'how-fxm-3-headline',
subhead: 'how-fxm-3-blurb',
localizedCta: LocaleUtils.fluentFormat(locales, 'download-firefox-banner-button'),
href: 'https://www.mozilla.org/firefox',
eventCategory: 'About Page: Download Firefox',
download: 'download'
}
]
aboutPageStrings.forEach(aboutBlock => {
aboutBlock.headline = LocaleUtils.fluentFormat(locales, aboutBlock.headline);
aboutBlock.subhead = LocaleUtils.fluentFormat(locales, aboutBlock.subhead);
aboutBlock.localizedCta = LocaleUtils.fluentFormat(locales, aboutBlock.localizedCta);
});
return aboutPageStrings;
aboutBlock.headline = LocaleUtils.fluentFormat(locales, aboutBlock.headline)
aboutBlock.subhead = LocaleUtils.fluentFormat(locales, aboutBlock.subhead)
aboutBlock.localizedCta = LocaleUtils.fluentFormat(locales, aboutBlock.localizedCta)
})
return aboutPageStrings
}
module.exports = {
getAboutPageStrings,
getFooterLinks,
};
getFooterLinks
}

Просмотреть файл

@ -1,104 +1,99 @@
"use strict";
'use strict'
const AppConstants = require("./../app-constants");
const { LocaleUtils } = require("./../locale-utils");
const mozlog = require("./../log");
const AppConstants = require('./../app-constants')
const { LocaleUtils } = require('./../locale-utils')
const mozlog = require('./../log')
const log = mozlog('template-helpers/hbs-helpers')
const log = mozlog("template-helpers/hbs-helpers");
function getSupportedLocales(args) {
function getSupportedLocales (args) {
if (args.data) {
if (args.data.root.supportedLocales) {
return args.data.root.supportedLocales;
return args.data.root.supportedLocales
}
return args.data.root.req.supportedLocales;
return args.data.root.req.supportedLocales
}
if (args.this) {
return args.this.req.supportedLocales;
return args.this.req.supportedLocales
}
return null;
return null
}
function getFirstItem(arr) {
if (!arr) return;
if (typeof arr === "string") return arr.split(",")[0];
return arr[0];
function getFirstItem (arr) {
if (!arr) return
if (typeof arr === 'string') return arr.split(',')[0]
return arr[0]
}
function vpnPromoBlocked(args) {
const userLocales = getSupportedLocales(args);
return AppConstants.VPN_PROMO_BLOCKED_LOCALES?.some(blockedLocale => userLocales[0].includes(blockedLocale));
function vpnPromoBlocked (args) {
const userLocales = getSupportedLocales(args)
return AppConstants.VPN_PROMO_BLOCKED_LOCALES?.some(blockedLocale => userLocales[0].includes(blockedLocale))
}
function englishInAcceptLanguages(args) {
const acceptedLanguages = args.data.root.req.acceptsLanguages();
return acceptedLanguages.some(locale => locale.startsWith("en"));
function englishInAcceptLanguages (args) {
const acceptedLanguages = args.data.root.req.acceptsLanguages()
return acceptedLanguages.some(locale => locale.startsWith('en'))
}
function escapeHtmlAttributeChars(text) {
return text.replace(/"/g, "&quot;").replace(/'/g, "&#039;");
function escapeHtmlAttributeChars (text) {
return text.replace(/"/g, '&quot;').replace(/'/g, '&#039;')
}
function recruitmentBanner(args) {
function recruitmentBanner (args) {
if (!AppConstants.RECRUITMENT_BANNER_LINK || !AppConstants.RECRUITMENT_BANNER_TEXT) {
return;
return
}
if (!englishInAcceptLanguages(args)) {
return;
return
}
return `<div class="recruitment-banner"><a id="recruitment-banner" href="${AppConstants.RECRUITMENT_BANNER_LINK}" hidden target="_blank" rel="noopener noreferrer" data-ga-link="" data-event-category="Recruitment" data-event-label="${escapeHtmlAttributeChars(AppConstants.RECRUITMENT_BANNER_TEXT)}">${AppConstants.RECRUITMENT_BANNER_TEXT}</a></div>`;
return `<div class="recruitment-banner"><a id="recruitment-banner" href="${AppConstants.RECRUITMENT_BANNER_LINK}" hidden target="_blank" rel="noopener noreferrer" data-ga-link="" data-event-category="Recruitment" data-event-label="${escapeHtmlAttributeChars(AppConstants.RECRUITMENT_BANNER_TEXT)}">${AppConstants.RECRUITMENT_BANNER_TEXT}</a></div>`
}
function showCsatBanner(args) {
const signupDate = args.data.root.req.session.user?.created_at;
function showCsatBanner (args) {
const signupDate = args.data.root.req.session.user?.created_at
if (!signupDate) return; // don't show if user is not logged in or not signed up
if (!signupDate) return // don't show if user is not logged in or not signed up
if (args.data.root.req.cookies.csatHidden) return; // don't show if user closed banner
if (args.data.root.req.cookies.csatHidden) return // don't show if user closed banner
if (Date.now() - Date.parse(signupDate) < 604800000) return; // don't show if sign-up is less than 7 days old
if (Date.now() - Date.parse(signupDate) < 604800000) return // don't show if sign-up is less than 7 days old
if (AppConstants.RECRUITMENT_BANNER_LINK || AppConstants.RECRUITMENT_BANNER_TEXT) return; // don't show if recruitment banner is present
if (AppConstants.RECRUITMENT_BANNER_LINK || AppConstants.RECRUITMENT_BANNER_TEXT) return // don't show if recruitment banner is present
if (!englishInAcceptLanguages(args)) return; // don't show if language is not english
if (!englishInAcceptLanguages(args)) return // don't show if language is not english
return true;
return true
}
function getString(id, args) {
const supportedLocales = getSupportedLocales(args);
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
function getString (id, args) {
const supportedLocales = getSupportedLocales(args)
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
}
function getStringWithFallback(id, fallbackId, args) {
const supportedLocales = getSupportedLocales(args);
return LocaleUtils.fluentFormatWithFallback(supportedLocales, id, fallbackId, args.hash);
function getStringWithFallback (id, fallbackId, args) {
const supportedLocales = getSupportedLocales(args)
return LocaleUtils.fluentFormatWithFallback(supportedLocales, id, fallbackId, args.hash)
}
function getStrings(stringArr, locales) {
function getStrings (stringArr, locales) {
stringArr.forEach(string => {
const stringId = string.stringId;
string.stringId = LocaleUtils.fluentFormat(locales, stringId);
});
return stringArr;
const stringId = string.stringId
string.stringId = LocaleUtils.fluentFormat(locales, stringId)
})
return stringArr
}
function fluentFxa(id, args) {
const supportedLocales = args.data.root.req.supportedLocales;
function fluentFxa (id, args) {
const supportedLocales = args.data.root.req.supportedLocales
if (AppConstants.FXA_ENABLED) {
id = `fxa-${id}`;
id = `fxa-${id}`
}
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
}
function getStringID(id, number, args) {
function getStringID (id, number, args) {
// const supportedLocales = args.data.root.req.supportedLocales;
// id = `${id}${number}`;
// if (modifiedStringMap[id]) {
@ -107,120 +102,108 @@ function getStringID(id, number, args) {
// return LocaleUtils.fluentFormat(supportedLocales, id);
}
function localizedBreachDataClasses(dataClasses, locales) {
const localizedDataClasses = [];
function localizedBreachDataClasses (dataClasses, locales) {
const localizedDataClasses = []
for (const dataClass of dataClasses) {
localizedDataClasses.push(LocaleUtils.fluentFormat(locales, dataClass));
localizedDataClasses.push(LocaleUtils.fluentFormat(locales, dataClass))
}
return localizedDataClasses.join(", ");
return localizedDataClasses.join(', ')
}
function fluentNestedBold(id, args) {
const supportedLocales = args.data.root.req.supportedLocales;
function fluentNestedBold (id, args) {
const supportedLocales = args.data.root.req.supportedLocales
const addMarkup = (word) => {
return ` <span class="bold">${word}</span> `;
};
let localizedStrings = LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
if (args.hash.breachCount || args.hash.breachCount === 0) {
localizedStrings = localizedStrings.replace(/(\s[\d]+\s)/, addMarkup(args.hash.breachCount));
return ` <span class="bold">${word}</span> `
}
return localizedStrings;
let localizedStrings = LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
if (args.hash.breachCount || args.hash.breachCount === 0) {
localizedStrings = localizedStrings.replace(/(\s[\d]+\s)/, addMarkup(args.hash.breachCount))
}
return localizedStrings
}
function prettyDate(date, locales) {
const jsDate = new Date(date);
const options = { year: "numeric", month: "long", day: "numeric" };
const intlDateTimeFormatter = new Intl.DateTimeFormat(locales, options);
return intlDateTimeFormatter.format(jsDate);
function prettyDate (date, locales) {
const jsDate = new Date(date)
const options = { year: 'numeric', month: 'long', day: 'numeric' }
const intlDateTimeFormatter = new Intl.DateTimeFormat(locales, options)
return intlDateTimeFormatter.format(jsDate)
}
function localeString(numericInput, locales) {
const intlNumberFormatter = new Intl.NumberFormat(locales);
return intlNumberFormatter.format(numericInput);
function localeString (numericInput, locales) {
const intlNumberFormatter = new Intl.NumberFormat(locales)
return intlNumberFormatter.format(numericInput)
}
function getFxaUrl() {
return AppConstants.FXA_SETTINGS_URL;
function getFxaUrl () {
return AppConstants.FXA_SETTINGS_URL
}
function eachFromTo (ary, min, max, options) {
if (!ary || ary.length === 0) { return options.inverse(this) }
function eachFromTo(ary, min, max, options) {
if (!ary || ary.length === 0)
return options.inverse(this);
let result = "";
let result = ''
for (let i = min; i < max && i < ary.length; i++) {
result = result + options.fn(ary[i]);
result = result + options.fn(ary[i])
}
return result;
return result
}
function localize(locales, stringId, args) {
return LocaleUtils.fluentFormat(locales, stringId, args);
function localize (locales, stringId, args) {
return LocaleUtils.fluentFormat(locales, stringId, args)
}
function loop(from, to, inc, block) {
block = block || { fn: function () { return arguments[0]; } };
const data = block.data || { index: null };
let output = "";
function loop (from, to, inc, block) {
block = block || { fn: function () { return arguments[0] } }
const data = block.data || { index: null }
let output = ''
for (let i = from; i <= to; i += inc) {
data["index"] = i;
output += block.fn(i, { data: data });
data.index = i
output += block.fn(i, { data })
}
return output;
return output
}
function ifCompare(v1, operator, v2, options) {
//https://stackoverflow.com/questions/28978759/length-check-in-a-handlebars-js-if-conditional
function ifCompare (v1, operator, v2, options) {
// https://stackoverflow.com/questions/28978759/length-check-in-a-handlebars-js-if-conditional
const operators = {
">": v1 > v2 ? true : false,
">=": v1 >= v2 ? true : false,
"<": v1 < v2 ? true : false,
"<=": v1 <= v2 ? true : false,
"===": v1 === v2 ? true : false,
"&&": v1 && v2 ? true : false,
"||": v1 || v2 ? true : false,
"!|": !v1 || !v2 ? true : false,
"!!": !v1 && !v2 ? true : false,
};
'>': v1 > v2,
'>=': v1 >= v2,
'<': v1 < v2,
'<=': v1 <= v2,
'===': v1 === v2,
'&&': !!(v1 && v2),
'||': !!(v1 || v2),
'!|': !!(!v1 || !v2),
'!!': !!(!v1 && !v2)
}
if (operators.hasOwnProperty(operator)) {
if (operators[operator]) {
return options.fn(this);
return options.fn(this)
}
return options.inverse(this);
return options.inverse(this)
}
log.error("ifCompare", { message: `${operator} not found` });
return;
log.error('ifCompare', { message: `${operator} not found` })
}
function breachMath(lValue, operator = null, rValue = null) {
lValue = parseFloat(lValue);
let returnValue = lValue;
function breachMath (lValue, operator = null, rValue = null) {
lValue = parseFloat(lValue)
let returnValue = lValue
if (operator) {
rValue = parseFloat(rValue);
rValue = parseFloat(rValue)
returnValue = {
"+": lValue + rValue,
"-": lValue - rValue,
"*": lValue * rValue,
"/": lValue / rValue,
"%": lValue % rValue,
}[operator];
'+': lValue + rValue,
'-': lValue - rValue,
'*': lValue * rValue,
'/': lValue / rValue,
'%': lValue % rValue
}[operator]
}
return returnValue;
return returnValue
}
module.exports = {
recruitmentBanner,
englishInAcceptLanguages,
@ -242,5 +225,5 @@ module.exports = {
breachMath,
loop,
showCsatBanner,
vpnPromoBlocked,
};
vpnPromoBlocked
}

Просмотреть файл

@ -1,69 +1,68 @@
"use strict";
'use strict'
const { getStrings, getFxaUrl } = require("./hbs-helpers");
const { LocaleUtils } = require("./../locale-utils");
const { getStrings, getFxaUrl } = require('./hbs-helpers')
const { LocaleUtils } = require('./../locale-utils')
function getSignedInAs(args) {
const locales = args.data.root.req.supportedLocales;
const userEmail = args.data.root.req.session.user.primary_email;
const signedInAs = LocaleUtils.fluentFormat(locales, "signed-in-as", {
userEmail: `<span class="nav-user-email">${userEmail}</span>`});
return signedInAs;
function getSignedInAs (args) {
const locales = args.data.root.req.supportedLocales
const userEmail = args.data.root.req.session.user.primary_email
const signedInAs = LocaleUtils.fluentFormat(locales, 'signed-in-as', { userEmail: `<span class="nav-user-email">${userEmail}</span>` })
return signedInAs
}
function navLinks(args) {
const hostUrl = args.data.root.req.url;
const serverUrl = args.data.root.constants.SERVER_URL;
const locales = args.data.root.req.supportedLocales;
function navLinks (args) {
const hostUrl = args.data.root.req.url
const serverUrl = args.data.root.constants.SERVER_URL
const locales = args.data.root.req.supportedLocales
const links = [
{
title: "Home",
stringId: "home",
title: 'Home',
stringId: 'home',
href: `${serverUrl}/`,
activeLink: (hostUrl === "/" || hostUrl === "/dashboard"),
activeLink: (hostUrl === '/' || hostUrl === '/dashboard')
},
{
title: "Breaches",
stringId: "breaches",
title: 'Breaches',
stringId: 'breaches',
href: `${serverUrl}/breaches`,
activeLink: (hostUrl === "/breaches"),
activeLink: (hostUrl === '/breaches')
},
{
title: "Security Tips",
stringId: "security-tips",
title: 'Security Tips',
stringId: 'security-tips',
href: `${serverUrl}/security-tips`,
activeLink: (hostUrl === "/security-tips"),
},
];
const headerLinks = getStrings(links, locales);
return headerLinks;
activeLink: (hostUrl === '/security-tips')
}
]
const headerLinks = getStrings(links, locales)
return headerLinks
}
function fxaMenuLinks(args) {
const locales = args.data.root.req.supportedLocales;
function fxaMenuLinks (args) {
const locales = args.data.root.req.supportedLocales
const fxaLinks = [
{
title: "Preferences",
stringId: "preferences",
href: "/user/preferences",
title: 'Preferences',
stringId: 'preferences',
href: '/user/preferences'
},
{
title: "Firefox Account",
stringId: "fxa-account",
href: getFxaUrl(),
title: 'Firefox Account',
stringId: 'fxa-account',
href: getFxaUrl()
},
{
title: "Sign Out",
stringId: "sign-out",
href: "/user/logout",
},
];
title: 'Sign Out',
stringId: 'sign-out',
href: '/user/logout'
}
]
return getStrings(fxaLinks, locales);
return getStrings(fxaLinks, locales)
}
module.exports = {
navLinks,
fxaMenuLinks,
getSignedInAs,
};
getSignedInAs
}

Просмотреть файл

@ -1,36 +1,35 @@
"use strict";
'use strict'
const { LocaleUtils } = require("./../locale-utils");
const { LocaleUtils } = require('./../locale-utils')
function makeLanding(args) {
const locales = args.data.root.req.supportedLocales;
const featuredBreach = args.data.root.featuredBreach;
function makeLanding (args) {
const locales = args.data.root.req.supportedLocales
const featuredBreach = args.data.root.featuredBreach
const landingCopy = {};
const landingCopy = {}
if (featuredBreach) {
landingCopy.headline = LocaleUtils.fluentFormat(locales, "was-your-info-exposed", { breachName : `<span class="bold">${featuredBreach.Title}</span>` });
landingCopy.headline = LocaleUtils.fluentFormat(locales, 'was-your-info-exposed', { breachName: `<span class="bold">${featuredBreach.Title}</span>` })
landingCopy.info = [
{
subhead: LocaleUtils.fluentFormat(locales, "about-fxm-headline"),
body: LocaleUtils.fluentFormat(locales, "about-fxm-blurb"),
},
];
subhead: LocaleUtils.fluentFormat(locales, 'about-fxm-headline'),
body: LocaleUtils.fluentFormat(locales, 'about-fxm-blurb')
}
]
} else {
landingCopy.headline = LocaleUtils.fluentFormat(locales, "see-if-youve-been-part");
landingCopy.subhead = LocaleUtils.fluentFormat(locales, "find-out-what-hackers-know");
landingCopy.headline = LocaleUtils.fluentFormat(locales, 'see-if-youve-been-part')
landingCopy.subhead = LocaleUtils.fluentFormat(locales, 'find-out-what-hackers-know')
}
if (featuredBreach && featuredBreach.IsSensitive) {
landingCopy.breachIsSensitive = true;
landingCopy.breachIsSensitive = true
landingCopy.info.unshift({
subhead: LocaleUtils.fluentFormat(locales, "sensitive-sites"),
body: LocaleUtils.fluentFormat(locales, "sensitive-sites-copy"),
});
subhead: LocaleUtils.fluentFormat(locales, 'sensitive-sites'),
body: LocaleUtils.fluentFormat(locales, 'sensitive-sites-copy')
})
}
return args.fn(landingCopy);
return args.fn(landingCopy)
}
module.exports = {
makeLanding,
};
makeLanding
}

Просмотреть файл

@ -1,18 +1,18 @@
"use strict";
'use strict'
const articles = require("./articles");
const breachDetail = require("./breach-detail");
const breaches = require("./breaches");
const breachStats = require("./breach-stats");
const dashboard = require("./dashboard");
const emails = require("./emails");
const footer = require("./footer");
const header = require("./header");
const homepage = require("./homepage");
const legacyHelpers = require("./hbs-helpers");
const scanResults = require("./scan-results");
const signUpBanners = require("./sign-up-banners");
const productEducationVideo = require("./product-education-video");
const articles = require('./articles')
const breachDetail = require('./breach-detail')
const breaches = require('./breaches')
const breachStats = require('./breach-stats')
const dashboard = require('./dashboard')
const emails = require('./emails')
const footer = require('./footer')
const header = require('./header')
const homepage = require('./homepage')
const legacyHelpers = require('./hbs-helpers')
const scanResults = require('./scan-results')
const signUpBanners = require('./sign-up-banners')
const productEducationVideo = require('./product-education-video')
module.exports = {
helpers: Object.assign(
@ -29,5 +29,5 @@ module.exports = {
scanResults,
signUpBanners,
productEducationVideo
),
};
)
}

Просмотреть файл

@ -1,15 +1,15 @@
"use strict";
'use strict'
const AppConstants = require("../app-constants");
const { getString } = require("./hbs-helpers");
const AppConstants = require('../app-constants')
const { getString } = require('./hbs-helpers')
function productEducation(type, args) {
const headingTxt = type.toLowerCase() === "relay" ? getString("ad-unit-1-how-do-you-keep", args) : getString("ad-unit-2-do-you-worry", args);
function productEducation (type, args) {
const headingTxt = type.toLowerCase() === 'relay' ? getString('ad-unit-1-how-do-you-keep', args) : getString('ad-unit-2-do-you-worry', args)
return {
headingTxt,
videoSrc: AppConstants[`EDUCATION_VIDEO_URL_${type.toUpperCase()}`],
};
videoSrc: AppConstants[`EDUCATION_VIDEO_URL_${type.toUpperCase()}`]
}
}
module.exports = { productEducation };
module.exports = { productEducation }

Просмотреть файл

@ -1,75 +1,73 @@
"use strict";
'use strict'
const AppConstants = require("./../app-constants");
const AppConstants = require('./../app-constants')
const { localize } = require("./hbs-helpers");
const { localize } = require('./hbs-helpers')
function productPromos(locales, promoUtms, promoKey) {
function productPromos (locales, promoUtms, promoKey) {
const productPromos = {
"monitor": {
promoHeadline: localize(locales, "monitor-promo-headline"),
promoBody: localize(locales, "monitor-promo-body"),
promoCta: localize(locales, "sign-up-for-alerts"),
promoId: "promo-monitor",
monitor: {
promoHeadline: localize(locales, 'monitor-promo-headline'),
promoBody: localize(locales, 'monitor-promo-body'),
promoCta: localize(locales, 'sign-up-for-alerts'),
promoId: 'promo-monitor',
promoUrl: `${AppConstants.SERVER_URL}/oauth/init` + promoUtms,
fxaEntrypoint: true,
fxaEntrypoint: true
},
"fx-mobile": {
promoHeadline: localize(locales, "mobile-promo-headline"),
promoBody: localize(locales, "mobile-promo-body"),
promoCta: localize(locales, "mobile-promo-cta"),
promoId: "promo-mobile",
promoUrl: "http://mozilla.org/firefox/mobile" + promoUtms,
'fx-mobile': {
promoHeadline: localize(locales, 'mobile-promo-headline'),
promoBody: localize(locales, 'mobile-promo-body'),
promoCta: localize(locales, 'mobile-promo-cta'),
promoId: 'promo-mobile',
promoUrl: 'http://mozilla.org/firefox/mobile' + promoUtms
},
"fpn": {
promoHeadline: localize(locales, "fpn-promo-headline"),
promoBody: localize(locales, "promo-fpn-body"),
promoCta: localize(locales, "promo-fpn-cta"),
promoId: "promo-fpn",
promoUrl: "https://fpn.firefox.com" + promoUtms,
fpn: {
promoHeadline: localize(locales, 'fpn-promo-headline'),
promoBody: localize(locales, 'promo-fpn-body'),
promoCta: localize(locales, 'promo-fpn-cta'),
promoId: 'promo-fpn',
promoUrl: 'https://fpn.firefox.com' + promoUtms
},
"fx-ecosystem": {
promoHeadline: localize(locales, "ecosystem-promo-headline"),
promoBody: localize(locales, "ecosystem-promo-body"),
promoCta: localize(locales, "promo-ecosystem-cta"),
promoId: "promo-ecosystem",
promoUrl: "https://www.mozilla.org/firefox" + promoUtms,
},
};
if (productPromos[promoKey]) {
return productPromos[promoKey];
'fx-ecosystem': {
promoHeadline: localize(locales, 'ecosystem-promo-headline'),
promoBody: localize(locales, 'ecosystem-promo-body'),
promoCta: localize(locales, 'promo-ecosystem-cta'),
promoId: 'promo-ecosystem',
promoUrl: 'https://www.mozilla.org/firefox' + promoUtms
}
}
productPromos["fx-ecosystem"];
if (productPromos[promoKey]) {
return productPromos[promoKey]
}
productPromos['fx-ecosystem']
}
function getPromoStrings(args) {
const templateData = args.data.root;
const locales = templateData.req.supportedLocales;
const breach = templateData.featuredBreach;
const promoUtms = "?utm_source=fx-monitor&utm_medium=referral&utm_campaign=promo-banner&utm_content=desktop";
function getPromoStrings (args) {
const templateData = args.data.root
const locales = templateData.req.supportedLocales
const breach = templateData.featuredBreach
const promoUtms = '?utm_source=fx-monitor&utm_medium=referral&utm_campaign=promo-banner&utm_content=desktop'
// show Monitor sign up promo if there is no signed in user
if (!templateData.req.session.user) {
return productPromos(locales, promoUtms, "monitor");
return productPromos(locales, promoUtms, 'monitor')
}
const userAgent = templateData.req.headers["user-agent"];
const userAgent = templateData.req.headers['user-agent']
const isBrowserFirefoxMobile = (
(/Mobile/i.test(userAgent) && /Firefox/i.test(userAgent))||
(/Mobile/i.test(userAgent) && /Firefox/i.test(userAgent)) ||
/FxiOS/i.test(userAgent)
);
const PRODUCT_PROMOS_ENABLED = (AppConstants.PRODUCT_PROMOS_ENABLED === "1");
)
const PRODUCT_PROMOS_ENABLED = (AppConstants.PRODUCT_PROMOS_ENABLED === '1')
if (PRODUCT_PROMOS_ENABLED) {
// show promo for mobile unless the user is on Firefox Mobile
if (!isBrowserFirefoxMobile) {
return productPromos(locales, promoUtms, "fx-mobile");
return productPromos(locales, promoUtms, 'fx-mobile')
}
// show promo for FPN if IP addresses were exposed
if (breach.DataClasses.includes("ip-addresses") && locales[0] === "en") {
return productPromos(locales, promoUtms, "fpn");
// show promo for FPN if IP addresses were exposed
if (breach.DataClasses.includes('ip-addresses') && locales[0] === 'en') {
return productPromos(locales, promoUtms, 'fpn')
}
// Don't show Lockwise banner until Monitor is whitelisted and UITour is implemented
@ -80,9 +78,9 @@ function getPromoStrings(args) {
// Return generic promo for Firefox's family of products
// by default if PRODUCT_PROMOS_ENABLED !-- "1"
return productPromos(locales, promoUtms, "fx-ecosystem");
return productPromos(locales, promoUtms, 'fx-ecosystem')
}
module.exports = {
getPromoStrings,
};
getPromoStrings
}

Просмотреть файл

@ -1,276 +1,277 @@
"use strict";
const { LocaleUtils } = require("./../locale-utils");
'use strict'
const { LocaleUtils } = require('./../locale-utils')
module.exports = {
getAllGenericRecommendations() {
getAllGenericRecommendations () {
return [
{
recommendationCopy: {
subhead: "rec-gen-1-subhead",
body: "rec-gen-1",
cta: "rec-gen-1-cta",
subhead: 'rec-gen-1-subhead',
body: 'rec-gen-1',
cta: 'rec-gen-1-cta'
},
ctaHref: "https://monitor.firefox.com/security-tips#strong-passwords",
ctaHref: 'https://monitor.firefox.com/security-tips#strong-passwords',
ctaShouldOpenNewTab: false,
ctaAnalyticsId: "How to create strong passwords",
recIconClassName: "rec-gen-1",
ctaAnalyticsId: 'How to create strong passwords',
recIconClassName: 'rec-gen-1'
},
{
recommendationCopy: {
subhead: "rec-gen-2-subhead",
body: "rec-gen-2",
cta: "rec-gen-2-cta",
subhead: 'rec-gen-2-subhead',
body: 'rec-gen-2',
cta: 'rec-gen-2-cta'
},
ctaHref: "https://monitor.firefox.com/security-tips#five-myths",
ctaHref: 'https://monitor.firefox.com/security-tips#five-myths',
ctaShouldOpenNewTab: false,
ctaAnalyticsId: "Myths about password managers",
recIconClassName: "rec-gen-2",
ctaAnalyticsId: 'Myths about password managers',
recIconClassName: 'rec-gen-2'
},
{
recommendationCopy: {
subhead: "rec-gen-3-subhead",
body: "rec-gen-3",
cta: "rec-gen-3-cta",
subhead: 'rec-gen-3-subhead',
body: 'rec-gen-3',
cta: 'rec-gen-3-cta'
},
ctaHref: "https://monitor.firefox.com/security-tips#steps-to-protect",
ctaHref: 'https://monitor.firefox.com/security-tips#steps-to-protect',
ctaShouldOpenNewTab: false,
ctaAnalyticsId: "Read more security tips",
recIconClassName: "rec-gen-3",
ctaAnalyticsId: 'Read more security tips',
recIconClassName: 'rec-gen-3'
},
{
recommendationCopy: {
subhead: "rec-gen-4-subhead",
body: "rec-gen-4",
subhead: 'rec-gen-4-subhead',
body: 'rec-gen-4'
},
recIconClassName: "rec-gen-4",
},
];
recIconClassName: 'rec-gen-4'
}
]
},
getAllPriorityDataClasses(isUserBrowserFirefox=false, isUserLocaleEnUs=false, isUserLocaleEn=false, changePWLink=null) {
return {
"government-issued-ids" : {
getAllPriorityDataClasses (isUserBrowserFirefox = false, isUserLocaleEnUs = false, isUserLocaleEn = false, changePWLink = null) {
return {
'government-issued-ids': {
weight: 101,
pathToGlyph: "svg/glyphs/social-security-numbers",
pathToGlyph: 'svg/glyphs/social-security-numbers'
},
"social-security-numbers" : {
'social-security-numbers': {
weight: 101,
pathToGlyph: "svg/glyphs/social-security-numbers",
recommendations: isUserLocaleEnUs ? [
{
recommendationCopy: {
subhead: "rec-ssn-cta",
cta: "rec-ssn-cta",
body: "rec-ssn",
},
ctaHref: "https://www.annualcreditreport.com/index.action",
ctaShouldOpenNewTab: true,
ctaAnalyticsId: "Request credit reports",
recIconClassName: "rec-ssn",
},
] : null,
pathToGlyph: 'svg/glyphs/social-security-numbers',
recommendations: isUserLocaleEnUs
? [
{
recommendationCopy: {
subhead: 'rec-ssn-cta',
cta: 'rec-ssn-cta',
body: 'rec-ssn'
},
ctaHref: 'https://www.annualcreditreport.com/index.action',
ctaShouldOpenNewTab: true,
ctaAnalyticsId: 'Request credit reports',
recIconClassName: 'rec-ssn'
}
]
: null
},
"passwords": {
passwords: {
weight: 100,
pathToGlyph: "svg/glyphs/passwords",
pathToGlyph: 'svg/glyphs/passwords',
recommendations: [
{
recommendationCopy : {
subhead: "rec-pw-1-subhead",
cta: changePWLink ? "rec-pw-1-cta" : "",
body: "rec-pw-1",
recommendationCopy: {
subhead: 'rec-pw-1-subhead',
cta: changePWLink ? 'rec-pw-1-cta' : '',
body: 'rec-pw-1'
},
ctaHref: changePWLink,
ctaShouldOpenNewTab: true,
ctaAnalyticsId: "Change password for this site",
recIconClassName: "rec-pw-1",
ctaAnalyticsId: 'Change password for this site',
recIconClassName: 'rec-pw-1'
},
{
recommendationCopy : {
subhead: "rec-pw-2-subhead",
recommendationCopy: {
subhead: 'rec-pw-2-subhead',
// Comment this CTA back in once monitor.firefox.com
// has been added to the whitelist and is able to open about:logins
// https://searchfox.org/mozilla-central/source/browser/app/permissions
// cta: isUserBrowserFirefox ? "rec-pw-2-cta-fx" : "",
body: "rec-pw-2",
body: 'rec-pw-2'
},
recIconClassName: "rec-pw-2",
recIconClassName: 'rec-pw-2'
// ctaHref: "", // Will open about:logins in the future or the lockwise website.
},
],
}
]
},
"bank-account-numbers": {
'bank-account-numbers': {
weight: 99,
pathToGlyph: "svg/glyphs/bank-account-numbers",
pathToGlyph: 'svg/glyphs/bank-account-numbers',
recommendations: [
{
recommendationCopy : {
subhead: "rec-bank-acc-subhead",
body: "rec-bank-acc",
recommendationCopy: {
subhead: 'rec-bank-acc-subhead',
body: 'rec-bank-acc'
},
recIconClassName: "rec-bank-acc",
},
],
recIconClassName: 'rec-bank-acc'
}
]
},
"credit-cards": {
'credit-cards': {
weight: 98,
pathToGlyph: "svg/glyphs/credit-cards",
pathToGlyph: 'svg/glyphs/credit-cards',
recommendations: [
{
recommendationCopy : {
subhead: "rec-cc-subhead",
body: "rec-cc",
recommendationCopy: {
subhead: 'rec-cc-subhead',
body: 'rec-cc'
},
recIconClassName: "rec-cc",
},
],
recIconClassName: 'rec-cc'
}
]
},
"credit-card-cvv": {
'credit-card-cvv': {
weight: 97,
pathToGlyph: "svg/glyphs/credit-card-cvvs",
pathToGlyph: 'svg/glyphs/credit-card-cvvs'
},
"partial-credit-card-data": {
'partial-credit-card-data': {
weight: 96,
pathToGlyph: "svg/glyphs/partial-credit-card-data",
pathToGlyph: 'svg/glyphs/partial-credit-card-data',
recommendations: [
{
recommendationCopy : {
subhead: "rec-cc-subhead",
body: "rec-cc",
recommendationCopy: {
subhead: 'rec-cc-subhead',
body: 'rec-cc'
},
recIconClassName: "rec-cc",
},
],
recIconClassName: 'rec-cc'
}
]
},
"ip-addresses": {
'ip-addresses': {
weight: 95,
pathToGlyph: "svg/glyphs/ip-addresses",
pathToGlyph: 'svg/glyphs/ip-addresses',
recommendations: [
{
recommendationCopy: {
subhead: "rec-ip-subhead",
cta: isUserLocaleEnUs ? "rec-moz-vpn-cta" : "",
body: isUserLocaleEnUs ? "rec-moz-vpn-update" : "rec-ip-non-us",
subhead: 'rec-ip-subhead',
cta: isUserLocaleEnUs ? 'rec-moz-vpn-cta' : '',
body: isUserLocaleEnUs ? 'rec-moz-vpn-update' : 'rec-ip-non-us'
},
ctaHref: "https://vpn.mozilla.org?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-recommendations",
ctaHref: 'https://vpn.mozilla.org?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-recommendations',
ctaShouldOpenNewTab: true,
ctaAnalyticsId: "Try Mozilla VPN",
recIconClassName: isUserLocaleEnUs ? "rec-ip-us" : "rec-ip-non-us",
},
],
ctaAnalyticsId: 'Try Mozilla VPN',
recIconClassName: isUserLocaleEnUs ? 'rec-ip-us' : 'rec-ip-non-us'
}
]
},
"historical-passwords": {
'historical-passwords': {
weight: 94,
pathToGlyph: "svg/glyphs/historical-passwords",
pathToGlyph: 'svg/glyphs/historical-passwords',
recommendations: [
{
recommendationCopy: {
subhead: "rec-hist-pw-subhead",
subhead: 'rec-hist-pw-subhead'
// Comment back in once Monitor is able to open about:logins
// cta: isUserBrowserFirefox ? "rec-hist-pw-cta-fx" : "",
},
recIconClassName: "rec-hist-pw",
recIconClassName: 'rec-hist-pw'
// Comment back in once Monitor is able to open about:logins
// ctaHref: "about:logins",
},
],
}
]
},
"security-questions-and-answers": {
'security-questions-and-answers': {
weight: 93,
pathToGlyph: "svg/glyphs/security-questions-and-answers",
pathToGlyph: 'svg/glyphs/security-questions-and-answers',
recommendations: [
{
recommendationCopy: {
subhead: "rec-sec-qa-subhead",
body: "rec-sec-qa",
subhead: 'rec-sec-qa-subhead',
body: 'rec-sec-qa'
},
recIconClassName: "rec-sec-qa",
},
],
recIconClassName: 'rec-sec-qa'
}
]
},
"phone-numbers": {
'phone-numbers': {
weight: 92,
pathToGlyph: "svg/glyphs/phone-numbers",
pathToGlyph: 'svg/glyphs/phone-numbers',
recommendations: [
{
recommendationCopy: {
subhead: "rec-phone-num-subhead",
body: "rec-phone-num",
subhead: 'rec-phone-num-subhead',
body: 'rec-phone-num'
},
recIconClassName: "rec-phone-num",
},
],
recIconClassName: 'rec-phone-num'
}
]
},
"email-addresses": {
'email-addresses': {
weight: 91,
pathToGlyph: "svg/glyphs/email-addresses",
pathToGlyph: 'svg/glyphs/email-addresses',
recommendations: [
{
recommendationCopy: {
subhead: "rec-email-subhead",
body: "rec-email",
cta: "rec-email-cta",
subhead: 'rec-email-subhead',
body: 'rec-email',
cta: 'rec-email-cta'
},
ctaHref: "https://relay.firefox.com/",
ctaHref: 'https://relay.firefox.com/',
ctaShouldOpenNewTab: true,
ctaAnalyticsId: "Try Firefox Relay",
recIconClassName: "rec-email",
},
],
ctaAnalyticsId: 'Try Firefox Relay',
recIconClassName: 'rec-email'
}
]
},
"dates-of-birth": {
'dates-of-birth': {
weight: 90,
pathToGlyph: "svg/glyphs/dates-of-birth",
pathToGlyph: 'svg/glyphs/dates-of-birth',
recommendations: [
{
recommendationCopy: {
subhead: "rec-dob-subhead",
body: "rec-dob",
subhead: 'rec-dob-subhead',
body: 'rec-dob'
},
recIconClassName: "rec-dob",
},
],
recIconClassName: 'rec-dob'
}
]
},
"pins": {
pins: {
weight: 89,
pathToGlyph: "svg/glyphs/pins",
pathToGlyph: 'svg/glyphs/pins',
recommendations: [
{
recommendationCopy: {
subhead: "rec-pins-subhead",
body: "rec-pins",
subhead: 'rec-pins-subhead',
body: 'rec-pins'
},
recIconClassName: "rec-pins",
},
],
recIconClassName: 'rec-pins'
}
]
},
"physical-addresses": {
'physical-addresses': {
weight: 88,
pathToGlyph: "svg/glyphs/physical-addresses",
pathToGlyph: 'svg/glyphs/physical-addresses',
recommendations: [
{
recommendationCopy: {
subhead: "rec-address-subhead",
body: "rec-address",
subhead: 'rec-address-subhead',
body: 'rec-address'
},
recIconClassName: "rec-address",
},
],
},
};
recIconClassName: 'rec-address'
}
]
}
}
},
getFourthPasswordRecommendation(locales) {
getFourthPasswordRecommendation (locales) {
return {
recommendationCopy: {
subhead: LocaleUtils.fluentFormat(locales, "rec-pw-4-subhead"),
body: LocaleUtils.fluentFormat(locales, "rec-pw-4"),
cta: LocaleUtils.fluentFormat(locales, "rec-pw-4-cta"),
subhead: LocaleUtils.fluentFormat(locales, 'rec-pw-4-subhead'),
body: LocaleUtils.fluentFormat(locales, 'rec-pw-4'),
cta: LocaleUtils.fluentFormat(locales, 'rec-pw-4-cta')
},
ctaHref: "https://2fa.directory/",
ctaHref: 'https://2fa.directory/',
ctaShouldOpenNewTab: true,
ctaAnalyticsId: "See sites that offer 2FA",
recIconClassName: "rec-pw-4",
};
},
};
ctaAnalyticsId: 'See sites that offer 2FA',
recIconClassName: 'rec-pw-4'
}
}
}

Просмотреть файл

@ -1,64 +1,63 @@
"use strict";
'use strict'
const { LocaleUtils } = require("./../locale-utils");
const { fluentNestedBold, getString } = require("./hbs-helpers");
const { LocaleUtils } = require('./../locale-utils')
const { fluentNestedBold, getString } = require('./hbs-helpers')
function getScanResultsHeadline(args) {
const locales = args.data.root.req.supportedLocales;
const featuredBreach = args.data.root.specificBreach;
const userCompromised = args.data.root.userCompromised;
const foundBreaches = args.data.root.foundBreaches;
function getScanResultsHeadline (args) {
const locales = args.data.root.req.supportedLocales
const featuredBreach = args.data.root.specificBreach
const userCompromised = args.data.root.userCompromised
const foundBreaches = args.data.root.foundBreaches
const headlineStrings = {
"headline": "",
"subhead": "",
};
headline: '',
subhead: ''
}
args.hash = {
breachName: `<span class="bold">${featuredBreach.Title}</span>`,
breachCount: foundBreaches.length,
};
breachCount: foundBreaches.length
}
if (userCompromised) {
if (foundBreaches.length === 1) {
headlineStrings.headline = fluentNestedBold("fb-comp-only", args);
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, "no-other-breaches-found");
return args.fn(headlineStrings);
headlineStrings.headline = fluentNestedBold('fb-comp-only', args)
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, 'no-other-breaches-found')
return args.fn(headlineStrings)
}
headlineStrings.headline = fluentNestedBold("fb-comp-and-others", args);
return args.fn(headlineStrings);
headlineStrings.headline = fluentNestedBold('fb-comp-and-others', args)
return args.fn(headlineStrings)
}
if (foundBreaches.length === 0) {
headlineStrings.headline = fluentNestedBold("fb-not-comp", args);
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, "no-other-breaches-found");
return args.fn(headlineStrings);
headlineStrings.headline = fluentNestedBold('fb-not-comp', args)
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, 'no-other-breaches-found')
return args.fn(headlineStrings)
}
headlineStrings.headline = fluentNestedBold("fb-not-comp", args);
headlineStrings.subhead = fluentNestedBold("other-known-breaches-found", args);
return args.fn(headlineStrings);
headlineStrings.headline = fluentNestedBold('fb-not-comp', args)
headlineStrings.subhead = fluentNestedBold('other-known-breaches-found', args)
return args.fn(headlineStrings)
}
function getFacebookResultMessage(id, args) {
let message = getString(id, args);
message = message.replace("<span>", "<span class='bold'>");
let ctaHref = "";
function getFacebookResultMessage (id, args) {
let message = getString(id, args)
message = message.replace('<span>', "<span class='bold'>")
let ctaHref = ''
switch (id) {
case "facebook-breach-what-to-do-1-headline":
ctaHref = "https://blog.mozilla.org/firefox/facebook-data-leak-explained/";
break;
case "facebook-breach-what-to-do-2-headline":
ctaHref = "https://blog.mozilla.org/firefox/mozilla-explains-sim-swapping/";
break;
case 'facebook-breach-what-to-do-1-headline':
ctaHref = 'https://blog.mozilla.org/firefox/facebook-data-leak-explained/'
break
case 'facebook-breach-what-to-do-2-headline':
ctaHref = 'https://blog.mozilla.org/firefox/mozilla-explains-sim-swapping/'
break
}
message = message.replace("<a>", `<a target="_blank" href="${ctaHref}">`);
message = message.replace('<a>', `<a target="_blank" href="${ctaHref}">`)
return message;
return message
}
module.exports = {
getScanResultsHeadline,
getFacebookResultMessage,
};
getFacebookResultMessage
}

Просмотреть файл

@ -1,64 +1,63 @@
"use strict";
'use strict'
const { LocaleUtils } = require("./../locale-utils");
const { LocaleUtils } = require('./../locale-utils')
function signUpBannerBulletPoints(args) {
const locales = args.data.root.req.supportedLocales;
function signUpBannerBulletPoints (args) {
const locales = args.data.root.req.supportedLocales
const bulletPoints = [
{
"title": "Enroll multiple emails in breach monitoring",
"stringId": "feat-enroll-multiple",
title: 'Enroll multiple emails in breach monitoring',
stringId: 'feat-enroll-multiple'
},
{
"title": "Advanced search in sensitive breaches",
"stringId": "feat-sensitive",
title: 'Advanced search in sensitive breaches',
stringId: 'feat-sensitive'
},
{
"string": "Security tips to protect your accounts",
"stringId": "feat-security-tips",
},
];
string: 'Security tips to protect your accounts',
stringId: 'feat-security-tips'
}
]
bulletPoints.forEach(bulletPoint => {
bulletPoint["translatedString"] = LocaleUtils.fluentFormat(locales, bulletPoint["stringId"]);
});
return bulletPoints;
bulletPoint.translatedString = LocaleUtils.fluentFormat(locales, bulletPoint.stringId)
})
return bulletPoints
}
function monitorFeaturesList(args) {
const locales = args.data.root.req.supportedLocales;
function monitorFeaturesList (args) {
const locales = args.data.root.req.supportedLocales
const features = [
{
title: "Stay alert to new breaches",
titleStringId: "stay-alert",
title: 'Stay alert to new breaches',
titleStringId: 'stay-alert',
subtitle: "If your information surfaces in a new data breach, we'll send you an alert.",
subtitleStringId: "if-your-info",
pictogramPath: "alert",
subtitleStringId: 'if-your-info',
pictogramPath: 'alert'
},
{
title: "Monitor several emails",
titleStringId: "monitor-several-emails",
subtitle: "Get ongoing breach monitoring for multiple email addresses.",
subtitleStringId: "get-ongoing-breach-monitoring",
pictogramPath: "email",
title: 'Monitor several emails',
titleStringId: 'monitor-several-emails',
subtitle: 'Get ongoing breach monitoring for multiple email addresses.',
subtitleStringId: 'get-ongoing-breach-monitoring',
pictogramPath: 'email'
},
{
title: "Protect your privacy",
titleStringId: "protect-your-privacy",
subtitle: "Find out what you need to do to keep your data safe from cyber criminals.",
subtitleStringId: "keep-your-data-safe",
pictogramPath: "advice",
},
];
title: 'Protect your privacy',
titleStringId: 'protect-your-privacy',
subtitle: 'Find out what you need to do to keep your data safe from cyber criminals.',
subtitleStringId: 'keep-your-data-safe',
pictogramPath: 'advice'
}
]
features.forEach(feature => {
feature.title = LocaleUtils.fluentFormat(locales, feature.titleStringId);
feature.subtitle = LocaleUtils.fluentFormat(locales, feature.subtitleStringId);
});
return features;
feature.title = LocaleUtils.fluentFormat(locales, feature.titleStringId)
feature.subtitle = LocaleUtils.fluentFormat(locales, feature.subtitleStringId)
})
return features
}
module.exports = {
monitorFeaturesList,
signUpBannerBulletPoints,
};
signUpBannerBulletPoints
}

Просмотреть файл

@ -1,24 +1,23 @@
"use strict";
'use strict'
const {vers, heartbeat} = require("../../controllers/dockerflow");
const { vers, heartbeat } = require('../../controllers/dockerflow')
test('GET __version__ calls sendFile', () => {
const mockRequest = {}
const mockResponse = { sendFile: jest.fn() }
test("GET __version__ calls sendFile", () => {
const mockRequest = {};
const mockResponse = { sendFile: jest.fn() };
vers(mockRequest, mockResponse)
vers(mockRequest, mockResponse);
const mockSendFileCallArgs = mockResponse.sendFile.mock.calls[0]
expect(mockSendFileCallArgs[0]).toContain('version.json')
})
const mockSendFileCallArgs = mockResponse.sendFile.mock.calls[0];
expect(mockSendFileCallArgs[0]).toContain("version.json");
});
test('GET __heartbeat__ calls send OK', () => {
const mockRequest = {}
const mockResponse = { send: jest.fn() }
test("GET __heartbeat__ calls send OK", () => {
const mockRequest = {};
const mockResponse = { send: jest.fn() };
heartbeat(mockRequest, mockResponse)
heartbeat(mockRequest, mockResponse);
const mockSendCallArgs = mockResponse.send.mock.calls[0];
expect(mockSendCallArgs[0]).toBe("OK");
});
const mockSendCallArgs = mockResponse.send.mock.calls[0]
expect(mockSendCallArgs[0]).toBe('OK')
})

Просмотреть файл

@ -1,140 +1,132 @@
"use strict";
'use strict'
const AppConstants = require("../../app-constants");
const DB = require("../../db/DB");
const HIBPLib = require("../../hibp");
const hibp = require("../../controllers/hibp");
const EmailUtils = require("../../email-utils");
const { LocaleUtils } = require("../../locale-utils");
const sha1 = require("../../sha1-utils");
const AppConstants = require('../../app-constants')
const DB = require('../../db/DB')
const HIBPLib = require('../../hibp')
const hibp = require('../../controllers/hibp')
const EmailUtils = require('../../email-utils')
const { LocaleUtils } = require('../../locale-utils')
const sha1 = require('../../sha1-utils')
const { testBreaches } = require("../test-breaches");
require("../resetDB");
const { testBreaches } = require('../test-breaches')
require('../resetDB')
test('notify POST without token should throw error', async () => {
const testEmail = 'victim@spoofattack.com'
const testHash = sha1(testEmail)
const testPrefix = testHash.slice(0, 6).toUpperCase()
const testSuffix = testHash.slice(6).toUpperCase()
test("notify POST without token should throw error", async() => {
const testEmail = "victim@spoofattack.com";
const testHash = sha1(testEmail);
const testPrefix = testHash.slice(0, 6).toUpperCase();
const testSuffix = testHash.slice(6).toUpperCase();
const mockRequest = { body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'SomeSensitiveBreach' } }
const mockResponse = { status: jest.fn(), json: jest.fn() }
const mockRequest = { body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "SomeSensitiveBreach" } };
const mockResponse = { status: jest.fn(), json: jest.fn() };
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('HIBP notify endpoint requires valid authorization token.')
})
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("HIBP notify endpoint requires valid authorization token.");
});
test('notify POST with invalid token should throw error', async () => {
const testEmail = 'victim@spoofattack.com'
const testHash = sha1(testEmail)
const testPrefix = testHash.slice(0, 6).toUpperCase()
const testSuffix = testHash.slice(6).toUpperCase()
const mockRequest = { token: 'token-that-doesnt-match-AppConstants', body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'SomeSensitiveBreach' } }
const mockResponse = { status: jest.fn(), json: jest.fn() }
test("notify POST with invalid token should throw error", async() => {
const testEmail = "victim@spoofattack.com";
const testHash = sha1(testEmail);
const testPrefix = testHash.slice(0, 6).toUpperCase();
const testSuffix = testHash.slice(6).toUpperCase();
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('HIBP notify endpoint requires valid authorization token.')
})
const mockRequest = { token: "token-that-doesnt-match-AppConstants", body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "SomeSensitiveBreach" } };
const mockResponse = { status: jest.fn(), json: jest.fn() };
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("HIBP notify endpoint requires valid authorization token.");
});
async function checkNotifyCallsEverythingItShould(breachedEmail, recipientEmail) {
async function checkNotifyCallsEverythingItShould (breachedEmail, recipientEmail) {
if (recipientEmail === undefined) {
recipientEmail = breachedEmail;
recipientEmail = breachedEmail
}
jest.mock("../../email-utils");
EmailUtils.sendEmail = jest.fn();
LocaleUtils.fluentFormat = jest.fn();
HIBPLib.getBreachesForEmail = jest.fn();
jest.mock('../../email-utils')
EmailUtils.sendEmail = jest.fn()
LocaleUtils.fluentFormat = jest.fn()
HIBPLib.getBreachesForEmail = jest.fn()
const testHash = sha1(breachedEmail);
const testPrefix = testHash.slice(0, 6).toUpperCase();
const testSuffix = testHash.slice(6).toUpperCase();
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: testBreaches, AVAILABLE_LANGUAGES: ["en"] } } };
const mockResponse = { status: jest.fn(), json: jest.fn() };
const testHash = sha1(breachedEmail)
const testPrefix = testHash.slice(0, 6).toUpperCase()
const testSuffix = testHash.slice(6).toUpperCase()
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: testBreaches, AVAILABLE_LANGUAGES: ['en'] } } }
const mockResponse = { status: jest.fn(), json: jest.fn() }
await hibp.notify(mockRequest, mockResponse);
await hibp.notify(mockRequest, mockResponse)
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls;
expect (mockFluentFormatCalls.length).toBe(1);
const mockFluentFormatCallArgs = mockFluentFormatCalls[0];
expect (mockFluentFormatCallArgs[0]).toEqual(["en"]);
expect (mockFluentFormatCallArgs[1]).toBe("breach-alert-subject");
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls
expect(mockFluentFormatCalls.length).toBe(1)
const mockFluentFormatCallArgs = mockFluentFormatCalls[0]
expect(mockFluentFormatCallArgs[0]).toEqual(['en'])
expect(mockFluentFormatCallArgs[1]).toBe('breach-alert-subject')
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls;
expect (mockSendEmailCalls.length).toBe(1);
const mockSendEmailCallArgs = mockSendEmailCalls[0];
expect (mockSendEmailCallArgs[0]).toBe(recipientEmail);
expect (mockSendEmailCallArgs[2]).toBe("default_email");
const mockStatusCallArgs = mockResponse.status.mock.calls[0];
expect(mockStatusCallArgs[0]).toBe(200);
const mockJsonCallArgs = mockResponse.json.mock.calls[0];
expect(mockJsonCallArgs[0].info).toContain("Notified");
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls
expect(mockSendEmailCalls.length).toBe(1)
const mockSendEmailCallArgs = mockSendEmailCalls[0]
expect(mockSendEmailCallArgs[0]).toBe(recipientEmail)
expect(mockSendEmailCallArgs[2]).toBe('default_email')
const mockStatusCallArgs = mockResponse.status.mock.calls[0]
expect(mockStatusCallArgs[0]).toBe(200)
const mockJsonCallArgs = mockResponse.json.mock.calls[0]
expect(mockJsonCallArgs[0].info).toContain('Notified')
}
test("good notify POST with breach, subscriber hash prefix and suffixes should call sendEmail and respond with 200", async () => {
const testEmail = "verifiedemail@test.com";
await checkNotifyCallsEverythingItShould(testEmail);
});
test('good notify POST with breach, subscriber hash prefix and suffixes should call sendEmail and respond with 200', async () => {
const testEmail = 'verifiedemail@test.com'
await checkNotifyCallsEverythingItShould(testEmail)
})
test('good notify POST with breach, secondary email hash prefix and suffixes should call sendEmail and respond with 200', async () => {
const testSecondaryEmail = 'firefoxaccount-secondary@test.com'
await checkNotifyCallsEverythingItShould(testSecondaryEmail)
})
test("good notify POST with breach, secondary email hash prefix and suffixes should call sendEmail and respond with 200", async () => {
const testSecondaryEmail = "firefoxaccount-secondary@test.com";
await checkNotifyCallsEverythingItShould(testSecondaryEmail);
});
test("good notify POST with breach, secondary email hash prefix and suffixes, all_emails_to_primary should call sendEmail to primary_email and respond with 200", async () => {
const testBreachedEmail = "secondary_sending_to_primary@test.com";
const expectedRecipientEmail = "all_emails_to_primary@test.com";
await checkNotifyCallsEverythingItShould(testBreachedEmail, expectedRecipientEmail);
});
test('good notify POST with breach, secondary email hash prefix and suffixes, all_emails_to_primary should call sendEmail to primary_email and respond with 200', async () => {
const testBreachedEmail = 'secondary_sending_to_primary@test.com'
const expectedRecipientEmail = 'all_emails_to_primary@test.com'
await checkNotifyCallsEverythingItShould(testBreachedEmail, expectedRecipientEmail)
})
// TODO: test("notify POST with unknown breach should successfully reload breaches")
test('notify POST with unknown breach should throw error', async () => {
jest.mock('../../hibp')
HIBPLib.loadBreachesIntoApp = jest.fn()
const testEmail = 'test@example.com'
const testHash = sha1(testEmail)
const testPrefix = testHash.slice(0, 6).toUpperCase()
const testSuffix = testHash.slice(6).toUpperCase()
test("notify POST with unknown breach should throw error", async () => {
jest.mock("../../hibp");
HIBPLib.loadBreachesIntoApp = jest.fn();
const testEmail = "test@example.com";
const testHash = sha1(testEmail);
const testPrefix = testHash.slice(0, 6).toUpperCase();
const testSuffix = testHash.slice(6).toUpperCase();
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: [] } } }
const mockResponse = { status: jest.fn(), json: jest.fn() }
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: [] } } };
const mockResponse = { status: jest.fn(), json: jest.fn() };
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('Unrecognized breach: test')
})
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("Unrecognized breach: test");
});
test('notify POST for subscriber with no signup_language should default to en', async () => {
jest.mock('../../email-utils')
jest.mock('../../hibp')
EmailUtils.sendEmail = jest.fn()
LocaleUtils.fluentFormat = jest.fn()
HIBPLib.subscribeHash = jest.fn()
const testEmail = 'subscriberwithoutlanguage@test.com'
test("notify POST for subscriber with no signup_language should default to en", async () => {
jest.mock("../../email-utils");
jest.mock("../../hibp");
EmailUtils.sendEmail = jest.fn();
LocaleUtils.fluentFormat = jest.fn();
HIBPLib.subscribeHash = jest.fn();
await DB.addSubscriber(testEmail)
const testEmail = "subscriberwithoutlanguage@test.com";
const testHash = sha1(testEmail)
const testPrefix = testHash.slice(0, 6).toUpperCase()
const testSuffix = testHash.slice(6).toUpperCase()
await DB.addSubscriber(testEmail);
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: testBreaches } } }
const mockResponse = { status: jest.fn(), json: jest.fn() }
const testHash = sha1(testEmail);
const testPrefix = testHash.slice(0, 6).toUpperCase();
const testSuffix = testHash.slice(6).toUpperCase();
await hibp.notify(mockRequest, mockResponse)
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: testBreaches } } };
const mockResponse = { status: jest.fn(), json: jest.fn() };
await hibp.notify(mockRequest, mockResponse);
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls;
expect (mockSendEmailCalls.length).toBe(1);
const mockSendEmailCallArgs = mockSendEmailCalls[0];
expect (mockSendEmailCallArgs[0]).toBe(testEmail);
expect (mockSendEmailCallArgs[2]).toBe("default_email");
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls;
const mockFluentFormatCallArgs = mockFluentFormatCalls[0];
expect (mockFluentFormatCallArgs[0]).toEqual(["en"]);
});
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls
expect(mockSendEmailCalls.length).toBe(1)
const mockSendEmailCallArgs = mockSendEmailCalls[0]
expect(mockSendEmailCallArgs[0]).toBe(testEmail)
expect(mockSendEmailCallArgs[2]).toBe('default_email')
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls
const mockFluentFormatCallArgs = mockFluentFormatCalls[0]
expect(mockFluentFormatCallArgs[0]).toEqual(['en'])
})

Просмотреть файл

@ -1,135 +1,131 @@
"use strict";
'use strict'
const AppConstants = require("../../app-constants");
const home = require("../../controllers/home");
const { getExperimentBranch } = require("../../controllers/utils");
const { scanResult } = require("../../scan-results");
const AppConstants = require('../../app-constants')
const home = require('../../controllers/home')
const { getExperimentBranch } = require('../../controllers/utils')
const { scanResult } = require('../../scan-results')
let mockRequest = { fluentFormat: jest.fn(), csrfToken: jest.fn() };
let mockRequest = { fluentFormat: jest.fn(), csrfToken: jest.fn() }
function mockRequestSessionReset(mockRequest) {
function mockRequestSessionReset (mockRequest) {
mockRequest.session.experimentFlags = {
excludeFromExperiment: false,
experimentBranch: false,
treatmentBranch: false,
controlBranch: false,
};
controlBranch: false
}
mockRequest.headers = {
"accept-language": "en",
};
'accept-language': 'en'
}
return mockRequest;
return mockRequest
}
function addBreachesToMockRequest(mockRequest) {
function addBreachesToMockRequest (mockRequest) {
const mockBreaches = [
{Name: "Test"},
{Name: "DontShow"},
];
mockRequest.app = { locals: { breaches: mockBreaches } };
return mockRequest;
{ Name: 'Test' },
{ Name: 'DontShow' }
]
mockRequest.app = { locals: { breaches: mockBreaches } }
return mockRequest
}
test("home GET without breach renders monitor without breach", () => {
mockRequest.query = { breach: null };
mockRequest = addBreachesToMockRequest(mockRequest);
mockRequest.session = { user: null} ;
const mockResponse = { render: jest.fn() };
test('home GET without breach renders monitor without breach', () => {
mockRequest.query = { breach: null }
mockRequest = addBreachesToMockRequest(mockRequest)
mockRequest.session = { user: null }
const mockResponse = { render: jest.fn() }
home.home(mockRequest, mockResponse);
home.home(mockRequest, mockResponse)
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
expect(mockRenderCallArgs[0]).toBe("monitor");
expect(mockRenderCallArgs[1].featuredBreach).toBe(null);
});
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
expect(mockRenderCallArgs[0]).toBe('monitor')
expect(mockRenderCallArgs[1].featuredBreach).toBe(null)
})
test('home GET with breach renders monitor with breach', async () => {
const testBreach = { Name: 'Test' }
mockRequest.query = { breach: testBreach.Name }
mockRequest = addBreachesToMockRequest(mockRequest)
mockRequest.session = { user: null }
mockRequest.url = { url: 'https://www.mozilla.com' }
mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL
test("home GET with breach renders monitor with breach", async() => {
const testBreach = {Name: "Test"};
mockRequest.query = { breach: testBreach.Name };
mockRequest = addBreachesToMockRequest(mockRequest);
mockRequest.session = { user: null };
mockRequest.url = { url: "https://www.mozilla.com" };
mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL;
const mockResponse = { render: jest.fn(), redirect: jest.fn() }
home.home(mockRequest, mockResponse)
const scanRes = await scanResult(mockRequest)
expect(scanRes.doorhangerScan).toBe(false)
expect(scanRes.selfScan).toBe(false)
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
expect(mockRenderCallArgs[0]).toBe('monitor')
expect(mockRenderCallArgs[1].featuredBreach).toEqual(testBreach)
})
const mockResponse = { render: jest.fn(), redirect: jest.fn() };
home.home(mockRequest, mockResponse);
const scanRes = await scanResult(mockRequest);
test('notFound set status 404 and renders 404', () => {
const mockResponse = { status: jest.fn(), render: jest.fn() }
expect(scanRes.doorhangerScan).toBe(false);
expect(scanRes.selfScan).toBe(false);
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
expect(mockRenderCallArgs[0]).toBe("monitor");
expect(mockRenderCallArgs[1].featuredBreach).toEqual(testBreach);
});
home.notFound(mockRequest, mockResponse)
const mockStatusCallArgs = mockResponse.status.mock.calls[0]
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
expect(mockStatusCallArgs[0]).toBe(404)
expect(mockRenderCallArgs[0]).toBe('subpage')
})
test("notFound set status 404 and renders 404", () => {
const mockResponse = { status: jest.fn(), render: jest.fn() };
home.notFound(mockRequest, mockResponse);
const mockStatusCallArgs = mockResponse.status.mock.calls[0];
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
expect(mockStatusCallArgs[0]).toBe(404);
expect(mockRenderCallArgs[0]).toBe("subpage");
});
test("Experiment Cohort Assignment Unit Test", () => {
test('Experiment Cohort Assignment Unit Test', () => {
// Resets session and language after each test
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// Set accept-language headers to German for first test.
mockRequest.headers = {
"accept-language": "de",
};
'accept-language': 'de'
}
// For this test, the split is 25/25/50 (Control, Treatment, Excluded)
const cohortVariation = {
"va": 25,
"vb": 25,
};
va: 25,
vb: 25
}
// The session is excluded from the experiment because the set language is German.
let experimentBranch = getExperimentBranch(mockRequest, false, ["en"], cohortVariation);
expect(experimentBranch).toBeFalsy();
// The session is excluded from the experiment because the set language is German.
let experimentBranch = getExperimentBranch(mockRequest, false, ['en'], cohortVariation)
expect(experimentBranch).toBeFalsy()
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// The session is assigned to the control group when the coin flip is 0;
let experimentNumber = 0;
let experimentNumber = 0
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
expect(experimentBranch).toBe("va");
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
expect(experimentBranch).toBe('va')
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// The session is assigned to the control group when the coin flip is 25;
experimentNumber = 25;
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
expect(experimentBranch).toBe("va");
experimentNumber = 25
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
expect(experimentBranch).toBe('va')
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// The session is assigned to the treatment group when the coin flip is 29;
experimentNumber = 26;
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
expect(experimentBranch).toBe("vb");
experimentNumber = 26
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
expect(experimentBranch).toBe('vb')
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// The session is assigned to the treatment group when the coin flip is 50;
experimentNumber = 50;
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
expect(experimentBranch).toBe("vb");
experimentNumber = 50
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
expect(experimentBranch).toBe('vb')
mockRequestSessionReset(mockRequest);
mockRequestSessionReset(mockRequest)
// The session is excluded from the experiment when the coin flip is 100
experimentNumber = 100;
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
expect(experimentBranch).toBeFalsy();
});
experimentNumber = 100
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
expect(experimentBranch).toBeFalsy()
})

Просмотреть файл

@ -1,132 +1,123 @@
"use strict";
'use strict'
const got = require("got");
const got = require('got')
const AppConstants = require("../../app-constants");
const DB = require("../../db/DB");
const EmailUtils = require("../../email-utils");
const getSha1 = require("../../sha1-utils");
const {init, confirmed} = require("../../controllers/oauth");
const AppConstants = require('../../app-constants')
const DB = require('../../db/DB')
const EmailUtils = require('../../email-utils')
const getSha1 = require('../../sha1-utils')
const { init, confirmed } = require('../../controllers/oauth')
require("../resetDB");
const {testBreaches} = require("../test-breaches");
require('../resetDB')
const { testBreaches } = require('../test-breaches')
jest.mock('got')
jest.mock("got");
const mockRequest = { fluentFormat: jest.fn() }
test('init request sets session cookie and redirects with access_type=offline', () => {
mockRequest.session = { }
mockRequest.query = { }
const mockResponse = { redirect: jest.fn() }
const mockRequest = { fluentFormat: jest.fn() };
init(mockRequest, mockResponse)
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
expect(mockRedirectCallArgs[0].href).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI)
expect(mockRedirectCallArgs[0].href).toMatch('access_type=offline')
})
test("init request sets session cookie and redirects with access_type=offline", () => {
mockRequest.session = { };
mockRequest.query = { };
const mockResponse = { redirect: jest.fn() };
init(mockRequest, mockResponse);
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
expect(mockRedirectCallArgs[0].href).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI);
expect(mockRedirectCallArgs[0].href).toMatch("access_type=offline");
});
function getMockRequest(userAddLanguages = "en-US,en;q=0.5", sessionState="test-state") {
function getMockRequest (userAddLanguages = 'en-US,en;q=0.5', sessionState = 'test-state') {
return {
app: { locals: { breaches: testBreaches } },
headers: { "accept-language": userAddLanguages },
headers: { 'accept-language': userAddLanguages },
fluentFormat: jest.fn(),
session: { state: sessionState },
originalUrl: "",
};
originalUrl: ''
}
}
test("confirmed request checks session cookie, calls FXA for token and email, adds new subscriber with signup language, and redirects", async () => {
const testFxAEmail = "fxa-new-user@test.com";
const userAddLanguages = "en-US,en;q=0.5";
const mockState = "123456789";
EmailUtils.sendEmail = jest.fn();
test('confirmed request checks session cookie, calls FXA for token and email, adds new subscriber with signup language, and redirects', async () => {
const testFxAEmail = 'fxa-new-user@test.com'
const userAddLanguages = 'en-US,en;q=0.5'
const mockState = '123456789'
EmailUtils.sendEmail = jest.fn()
// Mock the getToken, got, and render calls
const mockRequest = getMockRequest(userAddLanguages, mockState);
mockRequest.query = { state: mockState };
const mockResponse = { redirect: jest.fn()};
const mockFxAClient = { code : { getToken: jest.fn().mockReturnValueOnce({ accessToken: "testToken"}) } };
got.mockResolvedValue({ body: `{"email": "${testFxAEmail}"}` });
const mockRequest = getMockRequest(userAddLanguages, mockState)
mockRequest.query = { state: mockState }
const mockResponse = { redirect: jest.fn() }
const mockFxAClient = { code: { getToken: jest.fn().mockReturnValueOnce({ accessToken: 'testToken' }) } }
got.mockResolvedValue({ body: `{"email": "${testFxAEmail}"}` })
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient);
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient)
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0];
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl);
expect(mockFxACallArgs[1]).toEqual({state: mockState});
expect(mockRequest.session.state).toBeNull();
const mockGotCallArgs = got.mock.calls[0];
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI);
expect(mockGotCallArgs[1].headers.Authorization).toMatch("testToken");
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0]
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl)
expect(mockFxACallArgs[1]).toEqual({ state: mockState })
expect(mockRequest.session.state).toBeNull()
const mockGotCallArgs = got.mock.calls[0]
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI)
expect(mockGotCallArgs[1].headers.Authorization).toMatch('testToken')
const subscribers = await DB.getSubscribersByHashes([getSha1(testFxAEmail)]);
expect(subscribers[0].primary_verified).toBeTruthy();
expect(subscribers[0].primary_email).toBe(testFxAEmail);
expect(subscribers[0].signup_language).toBe(userAddLanguages);
const subscribers = await DB.getSubscribersByHashes([getSha1(testFxAEmail)])
expect(subscribers[0].primary_verified).toBeTruthy()
expect(subscribers[0].primary_email).toBe(testFxAEmail)
expect(subscribers[0].signup_language).toBe(userAddLanguages)
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
expect(mockRedirectCallArgs[0]).toBe("/user/dashboard");
});
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
expect(mockRedirectCallArgs[0]).toBe('/user/dashboard')
})
test("confirmed request checks session cookie, calls FXA for token and email, recognizes existing subscriber and redirects", async () => {
EmailUtils.sendEmail = jest.fn();
const mockState = "123456789";
const userAddLanguages = "en-US,en;q=0.5";
const mockRequest = getMockRequest(userAddLanguages, mockState);
mockRequest.query = { state: mockState };
const mockResponse = { redirect: jest.fn() };
const mockFxAClient = { code : { getToken: jest.fn().mockReturnValueOnce({ accessToken: "testToken"}) } };
test('confirmed request checks session cookie, calls FXA for token and email, recognizes existing subscriber and redirects', async () => {
EmailUtils.sendEmail = jest.fn()
const mockState = '123456789'
const userAddLanguages = 'en-US,en;q=0.5'
const mockRequest = getMockRequest(userAddLanguages, mockState)
mockRequest.query = { state: mockState }
const mockResponse = { redirect: jest.fn() }
const mockFxAClient = { code: { getToken: jest.fn().mockReturnValueOnce({ accessToken: 'testToken' }) } }
/* eslint-disable quotes */
got.mockResolvedValue({ body: `{"email": "firefoxaccount@test.com"}` });
/*eslint-enable quotes*/
got.mockResolvedValue({ body: `{"email": "firefoxaccount@test.com"}` })
/* eslint-enable quotes */
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient);
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient)
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0];
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl);
expect(mockFxACallArgs[1]).toEqual({state: mockState});
expect(mockRequest.session.state).toBeNull();
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0]
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl)
expect(mockFxACallArgs[1]).toEqual({ state: mockState })
expect(mockRequest.session.state).toBeNull()
const mockGotCallArgs = got.mock.calls[0];
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI);
expect(mockGotCallArgs[1].headers.Authorization).toMatch("testToken");
const mockGotCallArgs = got.mock.calls[0]
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI)
expect(mockGotCallArgs[1].headers.Authorization).toMatch('testToken')
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
expect(mockRedirectCallArgs[0]).toMatch("/");
});
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
expect(mockRedirectCallArgs[0]).toMatch('/')
})
test('confirmed request without session state cookie throws Error', async () => {
mockRequest.session = {}
const mockResponse = {}
test("confirmed request without session state cookie throws Error", async () => {
mockRequest.session = {};
const mockResponse = {};
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrowError('oauth-invalid-session')
})
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrowError("oauth-invalid-session");
});
test("confirmed request with no session state cookie throws Error", async () => {
test('confirmed request with no session state cookie throws Error', async () => {
// Mock request, but don't mock the getToken call to trigger the client-oauth2 error
mockRequest.session = { state: { } };
mockRequest.originalUrl = "";
const mockResponse = {};
mockRequest.session = { state: { } }
mockRequest.originalUrl = ''
const mockResponse = {}
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow("oauth-invalid-session");
});
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow('oauth-invalid-session')
})
test("confirmed request with bad session state cookie throws Error", async () => {
test('confirmed request with bad session state cookie throws Error', async () => {
// Mock request, but don't mock the getToken call to trigger the client-oauth2 error
mockRequest.session = { state: 12345 };
mockRequest.query = { state: 67890 };
mockRequest.originalUrl = "";
const mockResponse = {};
mockRequest.session = { state: 12345 }
mockRequest.query = { state: 67890 }
mockRequest.originalUrl = ''
const mockResponse = {}
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow("oauth-invalid-session");
});
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow('oauth-invalid-session')
})

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше