privly/privly-applications

View on GitHub
shared/javascripts/options.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * @fileOverview This file provides an fundamental interface to read and write
 * Privly extension options. In extension environment, it should be loaded into
 * every privly-application webpage and background pages.
 *
 * As a fundamental interface, it provides Privly.options.* to read and write
 * Privly extension options.
 * 
 * As a background script, it listens onInstall events to upgrade old option values
 * when user installs the extension and listens incoming Chrome messages to provide
 * interface to read and write extension options for content scripts and other pages.
 *
 * Chrome background sample incoming message:
 *   {ask: 'options/setPrivlyButtonEnabled', params: [true]}
 *   will call: Privly.options.setPrivlyButtonEnabled(true)
 *   return values are sent via message response.
 *
 * For more information about the whitelist, read:
 * https://github.com/privly/privly-organization/wiki/whitelist
 *
 * Local Storage Bindings Used:
 *
 * - options/injection {Boolean}
 *     Whether to enable content injection.
 *
 * - options/options/privlyButton {Boolean}
 *     Whether to enable Privly posting button. Privly posting button is a
 *     clickable button on the top-right corner of the editable element, allows
 *     user to create a Privly message conveniently.
 *
 * - options/whitelist/domains {[String]}
 *     An array of servers the user can provide to specify which servers they
 *     trust to automatically inject into the host page. This array is presented
 *     to the user every time they visit options, but the string used by the
 *     content script is `options/whitelist/regexp`.
 *
 * - options/whitelist/regexp {String}
 *     This string is formatted specifically so that privly.js can update its
 *     whitelist regexp.
 *
 * - options/contentServer/url {String}
 *     The content server the user will post to when generating new content.
 *
 * - options/glyph {Object} A consistent visual identifier to prevent spoofing
 *     color {String} The cell color of the glyph.
 *     cells {[Boolean]} The bitmap of the glyph cell.
 * 
 */
/*global chrome */
/*global Privly:true, ls */

// If Privly namespace is not initialized, initialize it
var Privly;
if (Privly === undefined) {
  Privly = {};
}

(function () {

  // If this file is already loaded, don't do it again
  if (Privly.options !== undefined) {
    return;
  }
  Privly.options = {};

  // CommonJS Module
  if (typeof module !== "undefined" && module.exports) {
    // load dependencies
    var storageModule = require("./storage.js");
    ls = storageModule.ls;
    Privly.storage = storageModule.storage;
    Privly.message = storageModule.message;
    // export interfaces
    module.exports.options = Privly.options;
    module.exports.storage = Privly.storage;
    module.exports.message = Privly.message;
  }


  /**
   * Broadcast option changed message
   * @param  {[type]} optionName
   * @param  {[type]} optionValue The new value of the option
   */
  function optionChanged(optionName, optionValue) {
    var message = {
      action: 'options/changed',
      option: optionName,
      newValue: optionValue
    };
    Privly.message.messageExtension(message);
    Privly.message.messageContentScripts(message);
  }

  /**
   * Update old option names to newer ones.
   * Will be called when user updates the extension.
   */
  Privly.options.upgrade = function () {
    // Privly posting button
    var disableButton = ls.getItem('Options:DissableButton');
    if (disableButton !== undefined) {
      try {
        Privly.options.setPrivlyButtonEnabled((disableButton !== true));
        ls.removeItem('Options:DissableButton');
      } catch (ignore) {}
    } else if(ls.getItem('options/privlyButton') === undefined){
      Privly.options.setPrivlyButtonEnabled(true);
    }
    // Whitelist CSV
    var userWhitelistCSV = ls.getItem('user_whitelist_csv');
    if (userWhitelistCSV !== undefined) {
      try {
        Privly.options.setWhitelist(userWhitelistCSV.split(','));
        ls.removeItem('user_whitelist_csv');
      } catch (ignore) {}
    }
    // Whitelist JSON
    var userWhitelistJSON = ls.getItem('user_whitelist_json');
    if (userWhitelistJSON !== undefined) {
      try {
        Privly.options.setWhitelist(userWhitelistJSON);
        ls.removeItem('user_whitelist_json');
      } catch (ignore) {}
    }
    // Content Server Url
    var postingContentServerUrl = ls.getItem('posting_content_server_url');
    if (postingContentServerUrl !== undefined) {
      try {
        Privly.options.setServerUrl(postingContentServerUrl);
        ls.removeItem('posting_content_server_url');
      } catch (ignore) {}
    }
    // Glyph
    var glyphColor = ls.getItem('glyph_color');
    var glyphCell = ls.getItem('glyph_cells');
    if (glyphColor !== undefined && glyphCell !== undefined) {
      // ls.setItem(..., "001122") would become number 1122 when calling ls.getItem(...)
      glyphColor = String(glyphColor);
      glyphColor = '000000'.substr(0, 6 - glyphColor.length) + glyphColor;
      try {
        Privly.options.setGlyph({
          color: glyphColor,
          cells: glyphCell.split(',').map(function (fill) {
            return (fill === 'true');
          })
        });
        ls.removeItem('glyph_color');
        ls.removeItem('glyph_cells');
      } catch (ignore) {}
    }
  };

  // If this script is running as a background script
  if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.getBackgroundPage) {
    // Set event listeners to execute upgrade() function when
    // the extension got installed.
    chrome.runtime.onInstalled.addListener(function () {
      Privly.options.upgrade();
    });
  }

  // Listen incoming messages to provide option interfaces
  // for content scripts
  function requestOptionInterface(request, sendResponse) {
    if (request.ask && request.ask.indexOf('options/') === 0) {
      var method = request.ask.split('/')[1];
      if (Privly.options[method] !== undefined) {
        var returnValue = Privly.options[method].apply(Privly.options, request.params);
        sendResponse(returnValue);
      }
    }
  }
  Privly.message.addListener(requestOptionInterface);

  /**
   * Whether Privly posting button is enabled
   * @return {Boolean}
   */
  Privly.options.isPrivlyButtonEnabled = function () {
    var v = Privly.storage.get('options/privlyButton');
    if (v === null) {
      v = false;
    }
    return v;
  };

  /**
   * Enable or disable the Privly posting button
   * @param {Boolean} enabled
   */
  Privly.options.setPrivlyButtonEnabled = function (enabled) {
    if (typeof enabled !== 'boolean') {
      throw new Error('invalid argument');
    }
    Privly.storage.set('options/privlyButton', enabled);
    optionChanged('options/isPrivlyButtonEnabled', enabled);
    return true;
  };

  /**
   * Whether content injection is enabled
   * @return {Boolean}
   */
  Privly.options.isInjectionEnabled = function () {
    var v = Privly.storage.get('options/injection');
    if (v === null) {
      v = true;
    }
    return v;
  };

  /**
   * Enable or diable content injection
   * @param {Boolean} enabled
   */
  Privly.options.setInjectionEnabled = function (enabled) {
    if (typeof enabled !== 'boolean') {
      throw new Error('invalid argument');
    }
    Privly.storage.set('options/injection', enabled);
    optionChanged('options/isInjectionEnabled', enabled);
    return true;
  };

  /**
   * Validates a FQDN
   * @param  {String}  domain Domain to validate
   * @return {Boolean}
   */
  Privly.options.isDomainValid = function (domain) {
    // Each subdomain can be from 1-63 characters and may contain alphanumeric
    // characters, - and _ but may not begin or end with - or _
    // Each domain can be from 1-63 characters and may contain alphanumeric 
    // characters and - but may not begin or end with - Each top level domain may
    // be from 2 to 9 characters and may contain alpha characters
    var validateSubdomain = /^(?!\-|_)[\w\-]{1,63}/g; //subdomains
    var validateDomain = /^(?!\-)[a-zA-Z0-9\-?]{1,63}$/g; //domain
    var validateDomainAndPort = /^(?!\-)[a-zA-Z0-9\-?]{1,63}(?::\d+)?$/g;
    var validateTLD = /^[a-zA-Z]{2,9}(?::\d+)?$/g; //top level domain

    //needed because js regex does not have look-behind
    var notEndInHyphenOrUnder = /[^\-_]$/g;
    var notEndInHyphen = /[^\-]$/g;

    var parts = domain.split(".");
    var valid_parts_count = 0;

    //iterate over domains, split by .
    var j;
    for (j = 0; j < parts.length; j++) {
      switch (j) {
      case parts.length - 1: // validate TLD or Domain if no TLD present
        if (parts.length === 1) {
          if (parts[j].match(validateDomainAndPort)) {
            valid_parts_count++;
          }
        } else {
          if (parts[j].match(validateTLD)) {
            valid_parts_count++;
          }
        }
        break;
      case parts.length - 2: // validate Domain
        if (parts[j].match(validateDomain) &&
            parts[j].match(notEndInHyphen)) {
          valid_parts_count++;
        }
        break;
      default: // validate Subdomain(s)
        if (parts[j].match(validateSubdomain) &&
            parts[j].match(notEndInHyphenOrUnder)) {
          valid_parts_count++;
        }
        break;
      }
    }

    //if all parts of domain are valid
    //append to regex for restricting domains of injected content
    return valid_parts_count === parts.length;
  };

  /**
   * Get user whitelist as JSON
   * @return {[String]} Array of whitelists
   */
  Privly.options.getWhitelistDomains = function () {
    var v = Privly.storage.get('options/whitelist/domains');
    if (v === null) {
      v = [];
    }
    return v;
  };

  /**
   * Get user whitelist as RegExp
   * @return {[String]} Array of whitelists
   */
  Privly.options.getWhitelistRegExp = function () {
    var v = Privly.storage.get('options/whitelist/regexp');
    if (v === null) {
      v = '';
    }
    return v;
  };

  /**
   * Update or set user whitelist
   * @param {String} whitelist
   */
  Privly.options.setWhitelist = function (whitelist) {
    // typeof null === 'object'
    if (typeof whitelist !== 'object' || whitelist === null || Object.prototype.toString.call(whitelist) !== "[object Array]") {
      throw new Error('invalid argument');
    }
    var regexp = ''; // stores regex to match validated domains
    var domains = whitelist.map(function (domain) {
      if (typeof domain !== 'string') {
        throw new Error('invalid argument');
      }
      domain = domain.toLowerCase();
      regexp += '|' + domain.replace(/\./g, '\\.') + '\\\/';
      if (!Privly.options.isDomainValid(domain)) {
        throw new Error('invalid domain');
      }
      return domain;
    });

    Privly.storage.set('options/whitelist/domains', domains);
    optionChanged('options/getWhitelistDomains', domains);
    Privly.storage.set('options/whitelist/regexp', regexp);
    optionChanged('options/getWhitelistRegExp', regexp);
    return true;
  };

  /**
   * Get content server Url
   * @return {String}
   */
  Privly.options.getServerUrl = function () {
    var v = Privly.storage.get('options/contentServer/url');
    if (v === null) {
      v = 'https://privlyalpha.org';
    }
    return v;
  };

  /**
   * Set content server Url
   * @param {String} url
   */
  Privly.options.setServerUrl = function (url) {
    if (typeof url !== 'string') {
      throw new Error('invalid argument');
    }
    Privly.storage.set('options/contentServer/url', url);
    optionChanged('options/getServerUrl', url);
    return true;
  };

  /**
   * Get user glyph cells and color
   * @return {Object|undefined}
   *   {String} color
   *   {[Boolean]} cells
   */
  Privly.options.getGlyph = function () {
    return Privly.storage.get('options/glyph');
  };

  /**
   * Set user glyph cells and color
   * @param {Object} glyph
   *   {String} color
   *   {[Boolean]} cells
   */
  Privly.options.setGlyph = function (glyph) {
    // typeof null === 'object'
    if (typeof glyph !== 'object' || glyph === null) {
      throw new Error('invalid argument');
    }
    // "glyph.cells.constructor !== Array" is not a safe check for "Array" data type.
    // Does not work if this script(options.js) is loaded in a different global environment than
    // glyph.js, i.e, when the scripts are loaded as CommonJS modules.
    if (typeof glyph.color !== 'string' || typeof glyph.cells !== 'object' || 
        glyph.cells === null || Object.prototype.toString.call(glyph.cells) !== "[object Array]") {
      throw new Error('invalid argument');
    }
    var obj = {
      color: glyph.color,
      cells: glyph.cells
    };
    Privly.storage.set('options/glyph', obj);
    optionChanged('options/getGlyph', obj);
    return true;
  };
}());