src/popup.js
/**
@typedef {Array<whitelist_entry>} whitelist A whitelist array
*/
/**
@desc Popup view for Burnt Chrome
*/
class Popup {
/**
@desc Initialize the instance.
@private
*/
constructor() {
// Setup defaults
/**
Background page.
*/
this.background = chrome.extension.getBackgroundPage();
/**
Template element ID
*/
this.templateId = 'frame';
/**
Static form data
*/
this.formData = {}
/**
Last error message
*/
this.lastErrorMessage = "";
//
this.setupTemplates();
// Render initial page
this.refresh();
}
/**
@desc Setup handlebars views to render compiled templates to.
@private
*/
setupTemplates() {
let handlebarsSelector = 'script[type="text/x-handlebars-template"]';
$(handlebarsSelector).each((index, $el) => {
// console.log($el, $el.id);
var templateId = $el.id;
if (!templateId) {
return console.warn("Unknown id for element: ", $el);
}
let containerId = templateId.split('-template')[0];
$($el).after($(`<div id="${containerId}"></div>`));
});
}
/**
@desc Generate the context to pass to the template to render.
@private
*/
getContext() {
return {
isLoggedIn: this.background.moderator.loggedIn,
isLocked: this.background.moderator.isLocked(),
isUnlocked: this.background.moderator.isUnlocked(),
whitelist: this.background.moderator.getWhitelistJSON(),
email: this.background.moderator.getEmail(),
errorMessage: this.lastErrorMessage
};
}
/**
@desc Render the template with a context
@private
*/
render(id, context) {
console.log('render', context);
let el = document.getElementById(`${id}-template`);
let source = el.innerHTML;
let template = Handlebars.compile(source);
let html = template(context);
document.getElementById(id).innerHTML = html;
// console.log('html', html);
// Bind events
this.bind();
}
/**
@desc Bind the events
@private
*/
bind() {
// console.log('Bind element events');
$('#lock').click(() => this.lock());
$('#logout').click(() => this.logout());
$('#unlock').click(() => this.unlock());
$('#add-to-whitelist').click(() => this.addToWhitelist());
$('.delete').click(() => this.removeFromWhitelist(event.target.id));
$('#loginIntro').click(() => this.loginIntro());
$('#listIntro').click(() => this.listIntro());
$('#lockedIntro').click(() => introJs().start());
$('#export-whitelist').click(() => {
let whitelistJSON = JSON.stringify(this.background.moderator.getWhitelistJSON());
let email = this.background.moderator.getEmail();
let fileName = `Whitelist for ${email}.json`;
this.downloadData(fileName, whitelistJSON, "application/json");
});
$('#import-whitelist-file').change((event) => {
let $el = $(event.currentTarget);
// Source: http://stackoverflow.com/a/13747921/2578205
let file = $el.prop('files')[0];
let reader = new FileReader();
reader.onload = () => {
let data = reader.result;
let json = JSON.parse(data);
// Add each entry to current whitelist
this.importWhitelist(json);
};
reader.readAsText(file);
});
$('#import-whitelist').click((event) => {
let url = $('#import-whitelist-url').val();
if (!url) {
// Invalid URL
return this.showError(new Error('Enter a URL to import whitelist from.'));
} else {
// Temporarily disable
this.background.moderator.disable();
let callback = (error) => {
if (error) {
this.showError(error);
}
// Re-enable
this.background.moderator.enable();
};
// Source: https://developers.google.com/web/updates/2015/03/introduction-to-fetch?hl=en
fetch(url)
.then((response) => {
if (response.status !== 200) {
callback(new Error(`An error occurred reading ${url}`));
} else {
response.json().then((data) => {
this.importWhitelist(data);
callback();
}).catch(callback);
}
})
.catch(callback);
}
});
// Clicking a whitelist entry fill in entry fields above
$('.whitelist-entry').click((event) => {
event.preventDefault();
// console.log('whitelist-entry click', event, this);
let $el = $(event.currentTarget);
let title = $el.data('title');
let url = $el.data('url');
$('input[name="entry-title"]').val(title).change();
$('input[name="entry-url"]').val(url).change();
});
// When Enter is pressed while focus is inside of password field
// Login automatically
$('input[name="password"]').keyup((event) => {
if (event.keyCode === 13) {
this.lock();
}
});
// When Enter is pressed while focus is inside of password field
// Login automatically
$('input[name="entry-url"]').keyup((event) => {
if (event.keyCode === 13) {
this.addToWhitelist();
}
});
// Make sure that input elements do not
// have their value cleared when template is refreshed.
// So every change to input, we store the state
// On refresh, we sync the last state of the input field
// back into the input field.
const attr = "name";
// We store state in formData property
const formData = this.formData;
// Iterate over all Input fields that should be syncing their value
$(`input[type="text"][${attr}]`).each((idx, el) => {
let $el = $(el);
// Get the key to sync with in the formData
let key = $el.attr(attr);
// Get current value of input
let val = _.get(formData, key);
$el.val(val);
// console.log($el, attr, key, val);
// Handle changing input value
// and storing state
let changeHandler = (event) => {
let text = $(event.currentTarget).val();
// console.log(key, text);
_.set(formData, key, text);
}
// Bind events to listen for changes to input field
$el.change(changeHandler);
$el.keyup(changeHandler);
});
}
/**
@desc Refresh the template
@private
*/
refresh() {
this.render(this.templateId, this.getContext());
}
/**
@desc Lock
@public
*/
lock() {
// console.log('Lock!');
let context = this.getContext();
let email = $('input[name="email"]').val() || context.email;
let password = $('input[name="password"]').val();
let emailPattern = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
let emailValid = (emailPattern.test(email) && email !== null);
let passwordValid = (password !== "" && password !== null);
$('#emailError').toggleClass("errorshow", !emailValid);
$('#passwordError').toggleClass("errorshow", !passwordValid);
if (emailValid && passwordValid) {
// console.log(email, password);
let successful = this.background.moderator.lock(email, password);
// console.log('successful', successful);
this.refresh();
introJs().exit();
$('#passwordError').toggleClass("errorshow", !successful);
}
}
/**
@desc Logout
@public
*/
logout() {
// console.log('logout!');
let successful = this.background.moderator.logout();
// console.log('successful', successful);
this.refresh();
introJs().exit();
}
/**
@desc Unlock
@public
*/
unlock() {
// console.log('Unlock!');
let context = this.getContext();
let email = context.email;
if (context.isLoggedIn) {
let successful = this.background.moderator.unlock(email, null);
this.refresh();
introJs().exit();
return;
}
let password = $('input[name="password"]').val();
let passwordValid = (password !== "" && password !== null);
$('#passwordError').toggleClass("errorshow", !passwordValid);
// console.log(email, password);
if (passwordValid) {
let successful = this.background.moderator.unlock(email, password);
this.refresh();
introJs().exit();
$('#passwordError').toggleClass("errorshow", !successful);
}
}
/**
@desc Add to Whitelist
@public
*/
addToWhitelist() {
// console.log('addToWhitelist');
let title = $('input[name="entry-title"]').val();
let url = $('input[name="entry-url"]').val();
try {
let successful = this.background.moderator.addToWhitelist(title, url);
if (successful) {
// Clear fields
$('input[name="entry-title"]').val('').change();
$('input[name="entry-url"]').val('').change();
// Refresh UI
this.refresh();
}
} catch (error) {
this.showError(error);
}
}
/**
@desc Remove entry from whitelist
@public
@param {string} url - URL pattern in an existing whitelist entry
*/
removeFromWhitelist(url) {
let successful = this.background.moderator.removeFromWhitelist(url);
if (successful) this.refresh();
}
/**
@desc Show error in User Interface
@public
@param {Error} error - The error to show
*/
showError(error) {
let message = error.message;
this.lastErrorMessage = message
this.refresh();
// $('#entryError').text(message).toggleClass("errorshow", !error);
}
/**
@desc Import entries from an existing whitelist
@public
@param {whitelist} whitelist - An existing whitelist to copy entries from
*/
importWhitelist(whitelist) {
_.each(whitelist, ({
title,
url
}) => {
try {
this.background.moderator.addToWhitelist(title, url);
} catch (error) {
this.showError(error);
}
});
this.refresh();
}
/**
@desc Show IntroJS tutorial for login page
*/
loginIntro() {
let intro = introJs();
intro.setOptions({
steps: [{
intro: 'Welcome to Burnt Chrome!<br>If this is your first time \
using Burnt Chrome, use these steps to get started!'
}, {
element: document.querySelector('input[name="email"]'),
intro: 'First lets create your account. Please enter a valid \
email address.',
position: 'right'
}, {
element: document.querySelector('input[name="password"]'),
intro: 'Next, choose the password you would like to use.',
position: 'right'
}, {
element: document.querySelector('#lock'),
intro: 'Click \"login and lock\" to login and setup your whitelist!',
position: 'left'
}]
});
intro.start();
}
/**
@desc Show IntroJS tutorial for whitelist list page
*/
listIntro() {
let intro = introJs();
intro.setOptions({
steps: [{
intro: 'Welcome to your whitelist!<br>Only websites on your list will be available \
while your browser is locked with Burnt Chrome. Lets get started by adding some entries.'
}, {
element: document.querySelector('input[name="entry-title"]'),
intro: 'First give your entry a title or short description.',
position: 'right'
}, {
element: document.querySelector('input[name="entry-url"]'),
intro: 'Next, enter the URL you wish to permit.',
position: 'right'
}, {
element: document.querySelector('#add-to-whitelist'),
intro: 'Click here to add your entry to the whitelist.',
position: 'right'
}, {
element: document.querySelector('#options'),
intro: 'Click here for more options.',
position: 'bottom'
}, {
element: document.querySelector('#export-whitelist'),
intro: 'Click here to export your current whitelist to a file.',
position: 'right'
}, {
element: document.querySelector('#import-whitelist-file'),
intro: 'Click here to import a whitelist from a file.',
position: 'right'
}, {
element: document.querySelector('#import-whitelist-url'),
intro: 'Enter a URL to read and import a remote whitelist from.',
position: 'right'
}, {
element: document.querySelector('#import-whitelist'),
intro: 'Click here to start importing a remote whitelist from the URL you previously entered.',
position: 'right'
}, {
element: document.querySelector('#logout'),
intro: 'When finished logout. Browsing will remain locked \
to your whitelist until you choose to unlock it.',
position: 'right'
}, {
element: document.querySelector('#unlock'),
intro: 'Click here to logout and unlock your browser.',
position: 'right'
}]
});
intro.start();
}
/**
@desc Download the given data to the users computer as a file
@param {string} name - Name of the file
@param {string} data - Contents of the file
@param {string} type - MIME type of the file
*/
downloadData(name, data, type) {
// Browser support
window.URL = window.URL || window.webkitURL;
// Arg defaults
type = type || "text/plain";
name = name || "download";
data = data || "";
// Create Blob
let blob;
if (data instanceof Blob) {
blob = data;
} else {
blob = new Blob([data], {
type: type
});
}
let url = window.URL.createObjectURL(blob)
// Create link
let link = document.createElement("a")
link.download = name
link.href = url
// Download!
// See http://stackoverflow.com/a/25047811/2578205 for more details
let event = document.createEvent("MouseEvents")
event.initMouseEvent(
"click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null
)
link.dispatchEvent(event)
}
};
/**
Ready up!
@ignore
*/
$(document).ready(() => {
// console.log('Ready');
let burnt = window.burnt = new Popup();
});