Home Reference Source Repository

src/background.js

/**
@typedef {Object} whitelist_entry A whitelist entry
@property {string} title The title of the whitelist entry.
@property {string} url The URL for the whitelist entry.
*/

/**
@typedef {Array<whitelist_entry>} whitelist A whitelist array
*/
if (typeof module !== 'undefined' && typeof require !== "undefined") {
  var _ = require('../../bower_components/lodash/dist/lodash');
  var minimatch = require('../../bower_components/minimatch/minimatch');
}

/**
@desc Whitelist component
*/
class Whitelist {

  /**
  @desc Initialize the instance.
  @private
  */
  constructor(email) {
    /**
    @desc Whitelist's Email
    @private
    */
    this.email = email;
  }

  /**
  @desc Add URL to whitelist.

  @param {string} title - Title for whitelist entry
  @param {string} url - URL for whitelist entry
  @return {boolean} Was successfully added to whitelist
  @throws {Error} URL is already in whitelist.
  @public
  */
  addURL(title, url) {
    // Trim whitespace from input
    title = title.trim();
    url = url.trim();
    // Validation entry input
    if (title === "") {
      throw new Error("Please enter a Title!");
    }
    if (url === "") {
      throw new Error("Please enter a URL!");
    }

    // Get whitelist
    let whitelist = this.get();

    // Check that URL is not already in whitelist
    if (_.findIndex(whitelist, (entry) => {
        return entry.url === url;
      }) !== -1) {
      // Must already be in whitelist
      // Throw an error!
      throw new Error(`URL pattern '${url}' is already in whitelist`);
    }
    // Not already in whitelist

    // Create new entry
    title = title || url;
    let entry = {
      title,
      url
    };
    // Add entry to whitelist
    whitelist.push(entry);
    // Save whitelist
    this.set(whitelist);
    // Return successful
    return true;
  }

  /**
  @desc Remove URL from whitelist
  @param {string} url - The URL to remove from the whitelist
  @return {whitelist} whitelist The updated whitelist
  @public
  */
  removeURL(url) {
    // Get whitelist
    let whitelist = this.get();
    // Find entry with matching URL in whitelist
    // Remove entry
    _.remove(whitelist, (entry) => {
      return url === entry.url
    });
    // Save new whitelist
    this.set(whitelist);
    return whitelist;
  }

  /**
  @desc Check if URL is allowed
  @param {string} url - The URL to check if whitelist allows.
  @return {boolean} Whether the URL is allowed.
  @public
  */
  isAllowed(url) {
    // Check if Chrome extension
    if (url.indexOf('chrome-extension://') !== -1) {
      return true;
    }
    // Get whitelist
    let whitelist = this.get();
    // Find entry with matching URL in whitelist
    let idx = _.findIndex(whitelist, (entry) => {
      try {
        return Whitelist.testURLs(url, entry.url);
      } catch (error) {
        console.error(error);
        return false;
      }
    });
    // Allow if not idx isnt -1
    console.log('isAllowed', url, idx !== -1);
    return idx !== -1;
  }

  /**
  @desc Get the whitelist in localStorage.

  @return {whitelist} The whitelist.
  @private
  */
  get() {
    return Whitelist.getWhitelistForEmail(this.email);
  }

  /**
  @desc Set the whitelist in localStorage.

  @param {whitelist} whitelist - The whitelist to save.
  @return {boolean} Whether successfully saved.
  @private
  */
  set(whitelist) {
    return Whitelist.setWhitelistForEmail(this.email, whitelist);
  }

  /**
  @desc Test URLs.
  @param {string} url - URL to test against.
  @param {string} pattern - URL pattern with glob support to test with.
  @return {boolean} If URL matches pattern
  @private
  @example
  testURLs('https://google.com','*://google.com/**'); // True
  testURLs('https://google.com','*://google.ca/**'); // False
  */
  static testURLs(url, pattern) {
    // Ensure that a URL ends with "/" such that "/**" matches
    // Check to make sure there is a path
    if (_.endsWith(pattern, '/**') && url.split('?').length === 1 && url[url.length - 1] !== '/') {
      url += '/';
    }
    // Handle cases where scheme is not provided in pattern
    const defaultScheme = "*";
    const schemeSep = "://";
    let urlParts = url.split(schemeSep);
    let patternParts = pattern.split(schemeSep);
    if (patternParts.length === 1) {
      patternParts = ["*", patternParts[0]];
    }
    return minimatch(urlParts[0], patternParts[0]) && minimatch(urlParts[1], patternParts[1]);
  }

  /**
  @desc Get the key to lookup in localStorage for the whitelist.
  @return {string} localStorage key for the whitelist for the given email.
  @private
  */
  static getWhitelistKeyForEmail(email) {
    return 'whitelist:' + email;
  }

  /**
  @desc Get the whitelist from localStorage
  @private
  @return {whitelist} The whitelist
  @see https://developer.chrome.com/extensions/storage
  @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
  */
  static getWhitelistForEmail(email) {
    let key = Whitelist.getWhitelistKeyForEmail(email);
    let whitelist = localStorage[key];
    if (typeof whitelist === 'string') {
      return JSON.parse(whitelist);
    }
    /*
      else if (typeof whitelist === 'object') {
      return whitelist;
    } */
    else {
      //console.warn('Unknown whitelist type: ', typeof whitelist, whitelist);
      return [];
    }
  }

  /**
  @desc Set the whitelist in localStorage
  @private
  @param {string} email - The admin's email address.
  @param {whitelist} whitelist - The whitelist to save.
  @return {boolean} Whether successfully saved.
  @see https://developer.chrome.com/extensions/storage
  @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
  */
  static setWhitelistForEmail(email, whitelist) {
    let key = Whitelist.getWhitelistKeyForEmail(email);
    if (typeof whitelist !== 'string') {
      whitelist = JSON.stringify(whitelist);
    }
    localStorage[key] = whitelist;
    // Return successful
    return true;
  }

}

/**
@desc Moderator component
*/
class Moderator {

  /**
  @desc Initialize the instance.
  @private
  */
  constructor() {

    /**
    @desc Logged in state
    */
    this.loggedIn = false;
    /**
    @desc Moderator is in disabled state
    */
    this.isDisabled = false;
    /**
    @desc The Whitelist instance
    @private
    */
    this.whitelist = null;

    // Event listeners
    this.setupListeners();


  }

  /**
  @desc Setup all event listeners.

  @see https://developer.chrome.com/extensions/webRequest
  @listens {onBeforeRequest} Listens for before requests
  @private
  */
  setupListeners() {

    // Request listener
    chrome.webRequest.onBeforeRequest.addListener((info) => {
      var url = info.url;
      let whitelist = this.getWhitelist();
      // console.log('webRequest', this, this.isDisabled, whitelist);
      if (this.isDisabled || !whitelist) {
        return {
          cancel: false
        };
      }
      let shouldAllow = whitelist.isAllowed(url);
      // console.log('URL: ', url, shouldAllow);
      return {
        cancel: !shouldAllow
      };
    }, {
      urls: ["<all_urls>"]
    }, ["blocking"]);

    // Message Passing listener
    // chrome.runtime.onMessage.addListener(
    //   (request, sender, sendResponse) => {
    //     console.log(sender.tab ?
    //       "from a content script:" + sender.tab.url :
    //       "from the extension");
    //     if (request.type == "login") {
    //       // Login!
    //       this.session.login((error, userInfo) => {
    //         console.log('userInfo', userInfo, error);
    //         sendResponse(userInfo);
    //       });
    //     } else {
    //       console.warn('Unknown message type', request);
    //     }
    //   });

  }

  /**
  @desc Disable moderator
  */
  disable() {
    this.isDisabled = true;
  }
  /**
  @desc Enable moderator
  */
  enable() {
    this.isDisabled = false;
  }

  /**
  @desc Check if Chrome is locked.
        Locked means that an admin has signed in with their email.
  @return {boolean} Whether Chrome is locked.
  @public
  */
  isLocked() {
    return !this.isUnlocked();
  }

  /**
  @desc Check if Chrome is unlocked.
        Locked means that an admin has signed in with their email.
  @return {boolean} Whether Chrome is unlocked.
  @public
  */
  isUnlocked() {
    return !this.getEmail();
  }

  /**
  @desc Lock
  @return {boolean} Whether successfully locked
  @public
  */
  lock(email, password) {
    // Check to make sure it is not already locked
    if (this.isUnlocked()) {
      // Not already locked
      this.setEmail(email);
      this.setPassword(password);
      return this.login(email, password);
    } else {
      return this.login(email, password);
    }
  }

  /**
  @desc Unlock
  @return {boolean} Whether successfully unlocked
  @public
  */
  unlock(email, password) {
    if (this.loggedIn || this.login(email, password)) {
      // Logged in now
      this.setEmail(null);
      this.setPassword(null);
      this.whitelist = null;
      return this.logout();
    } else {
      // Could not login to unlock
      return false;
    }
  }

  /**
  @desc Login
  @return {boolean} Whether successfully logged in
  @public
  */
  login(email, password) {
    var enteredHash = CryptoJS.SHA256(password);
    var passwordHash = enteredHash.toString(CryptoJS.enc.Base64)
    if (email === this.getEmail() && passwordHash === this.getPassword()) {
      this.loggedIn = true;
      this.whitelist = new Whitelist(this.getEmail());
      return true;
    } else {
      return false;
    }
  }

  /**
  @desc logout
  @return {boolean} Whether successfully logged out
  @public
  */
  logout() {
    this.loggedIn = false;
    return true;
  }

  /**
  @desc Get administator's email.
  @private
  @return {string} Admin email.
  */
  getEmail() {
      return localStorage['admin:email'];
    }
    /**
    @desc Set administrator's email.
    @private
    @param {string} email - The new email.
    @return {void} Void.
    */
  setEmail(email) {
    if (email === null) {
      delete localStorage['admin:email'];
    } else {
      localStorage['admin:email'] = email;
    }
  }

  /**
  @desc Get administator's password.
  @private
  @return {string} Admin password.
  */
  getPassword() {
      return localStorage['admin:password'];
    }
    /**
    @desc Set administrator's password.
    @private
    @param {string} password - The new password.
    @return {void} Void.
    */
  setPassword(password) {
    if (password === null) {
      delete localStorage['admin:password'];
    } else {
      var hash = CryptoJS.SHA256(password);
      localStorage['admin:password'] = hash.toString(CryptoJS.enc.Base64);
    }
  }

  /**
  @desc Get the current Whitelist instance.
  @return {Object} The whitelist instance.
  @private
  */
  getWhitelist() {
    return this.whitelist;
  }

  /**
  @desc Get the serialized JSON form of the Whitelist.
  @return {whitelist} The whitelist JSON.
  @public
  */
  getWhitelistJSON() {
    let whitelist = this.getWhitelist();
    if (whitelist) {
      return whitelist.get();
    } else {
      return [];
    }
  }

  /**
  @desc Add to Whitelist
  @return {boolean} Whether successfully added to whitelist.
  @public
  */
  addToWhitelist(title, url) {
    if (this.loggedIn && this.whitelist) {
      this.whitelist.addURL(title, url);
      return true;
    } else {
      return false;
    }
  }

  /**
  @desc Remove from Whitelist
  @return {boolean} Whether successfully removed from whitelist.
  @public
  */
  removeFromWhitelist(url) {
    if (this.loggedIn && this.whitelist) {
      this.whitelist.removeURL(url);
      return true;
    } else {
      return false;
    }
  }

}

/**
===== Main =====
Start the background script

@ignore
*/
if (typeof window !== 'undefined') {
  let moderator = new Moderator();
  window.moderator = moderator;
}

/*
Exporting for Node.js
*/
if (typeof module !== 'undefined') {
  module.exports = {
    Whitelist: Whitelist,
    Moderator: Moderator
  };
}