<commit_before>import {clipboard, nativeImage, remote, shell} from 'electron';
import {truncateString, matchesWord} from './utility';

const {Menu, MenuItem} = remote;

let d = require('debug')('electron-spellchecker:context-menu-builder');

const contextMenuStringTable = {
  copyMail: () => `Copy Email Address`,
  copyLinkUrl: () => `Copy Link`,
  openLinkUrl: () => `Open Link`,
  copyImageUrl: () => `Copy Image URL`,
  copyImage: () => `Copy Image`,
  addToDictionary: () => `Add to Dictionary`,
  lookUpDefinition: ({word}) => `Look Up "${word}"`,
  searchGoogle: () => `Search with Google`,
  cut: () => `Cut`,
  copy: () => `Copy`,
  paste: () => `Paste`,
  inspectElement: () => `Inspect Element`,
};

/**
 * ContextMenuBuilder creates context menus based on the content clicked - this
 * information is derived from
 * https://github.com/electron/electron/blob/master/docs/api/web-contents.md#event-context-menu,
 * which we use to generate the menu. We also use the spell-check information to
 * generate suggestions.
 */
export default class ContextMenuBuilder {
  /**
   * Creates an instance of ContextMenuBuilder
   *
   * @param  {SpellCheckHandler} spellCheckHandler  The spell checker to generate
   *                                                recommendations for.
   * @param  {BrowserWindow|WebView} windowOrWebView  The hosting window/WebView
   * @param  {Boolean} debugMode    If true, display the "Inspect Element" menu item.
   * @param  {function} processMenu If passed, this method will be passed the menu to change
   *                                it prior to display. Signature: (menu, info) => menu
   */
  constructor(spellCheckHandler, windowOrWebView=null, debugMode=false, processMenu=(m) => m) {
    this.spellCheckHandler = spellCheckHandler;
    this.debugMode = debugMode;
    this.processMenu = processMenu;
    this.menu = null;
    this.stringTable = Object.assign({}, contextMenuStringTable);

    windowOrWebView = windowOrWebView || remote.getCurrentWebContents();

    let ctorName = Object.getPrototypeOf(windowOrWebView).constructor.name;
    if (ctorName === 'WebContents') {
      this.getWebContents = () => windowOrWebView;
    } else {
      // NB: We do this because at the time a WebView is created, it doesn't
      // have a WebContents, we need to defer the call to getWebContents
      this.getWebContents = 'webContents' in windowOrWebView ?
        () => windowOrWebView.webContents :
        () => windowOrWebView.getWebContents();
    }
  }

  /**
   * Override the default logger for this class. You probably want to use
   * {{setGlobalLogger}} instead
   *
   * @param {Function} fn   The function which will operate like console.log
   */
  static setLogger(fn) {
    d = fn;
  }

  /**
   * Specify alternate string formatter for each context menu.
   * String table consist of string formatter as function instead per each context menu item,
   * allows to change string in runtime. All formatters are simply typeof () => string, except
   * lookUpDefinition provides word, ({word}) => string.
   *
   * @param {Object} stringTable The object contains string foramtter function for context menu.
   * It is allowed to specify only certain menu string as necessary, which will makes other string
   * fall backs to default.
   *
   */
  setAlternateStringFormatter(stringTable) {
    this.stringTable = Object.assign(this.stringTable, stringTable);
  }

  /**
   * Shows a popup menu given the information returned from the context-menu
   * event. This is probably the only method you need to call in this class.
   *
   * @param  {Object} contextInfo   The object returned from the 'context-menu'
   *                                Electron event.
   *
   * @return {Promise}              Completion
   */
  async showPopupMenu(contextInfo) {
    let menu = await this.buildMenuForElement(contextInfo);
    if (!menu) return;
    menu.popup(remote.getCurrentWindow(), { async: true });
  }

  /**
   * Builds a context menu specific to the given info that _would_ be shown
   * immediately by {{showPopupMenu}}. Use this to add your own menu items to
   * the list but use most of the default behavior.
   *
   * @return {Promise<Menu>}      The newly created `Menu`
   */
  async buildMenuForElement(info) {
    //d(`Got context menu event with args: ${JSON.stringify(info)}`);

    if (info.linkURL && info.linkURL.length > 0) {
      return this.buildMenuForLink(info);
    }

    if (info.hasImageContents && info.srcURL && info.srcURL.length > 1) {
      return this.buildMenuForImage(info);
    }

    if (info.isEditable || (info.inputFieldType && info.inputFieldType !== 'none')) {
      return await this.buildMenuForTextInput(info);
    }

    return this.buildMenuForText(info);
  }

  /**
   * Builds a menu applicable to a text input field.
   *
   * @return {Menu}  The `Menu`
   */
  async buildMenuForTextInput(menuInfo) {
    let menu = new Menu();

    await this.addSpellingItems(menu, menuInfo);
    this.addSearchItems(menu, menuInfo);

    this.addCut(menu, menuInfo);
    this.addCopy(menu, menuInfo);
    this.addPaste(menu, menuInfo);
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to a link element.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForLink(menuInfo) {
    let menu = new Menu();
    let isEmailAddress = menuInfo.linkURL.startsWith('mailto:');

    let copyLink = new MenuItem({
      label: isEmailAddress ? this.stringTable.copyMail() : this.stringTable.copyLinkUrl(),
      click: () => {
        // Omit the mailto: portion of the link; we just want the address
        clipboard.writeText(isEmailAddress ?
          menuInfo.linkText : menuInfo.linkURL);
      }
    });

    let openLink = new MenuItem({
      label: this.stringTable.openLinkUrl(),
      click: () => {
        //d(`Navigating to: ${menuInfo.linkURL}`);
        shell.openExternal(menuInfo.linkURL);
      }
    });

    menu.append(copyLink);
    menu.append(openLink);

    if (this.isSrcUrlValid(menuInfo)) {
      this.addSeparator(menu);
      this.addImageItems(menu, menuInfo);
    }

    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to a text field.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForText(menuInfo) {
    let menu = new Menu();

    this.addSearchItems(menu, menuInfo);
    this.addCopy(menu, menuInfo);
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to an image.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForImage(menuInfo) {
    let menu = new Menu();

    if (this.isSrcUrlValid(menuInfo)) {
      this.addImageItems(menu, menuInfo);
    }
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Checks if the current text selection contains a single misspelled word and
   * if so, adds suggested spellings as individual menu items.
   */
  async addSpellingItems(menu, menuInfo) {
    let target = this.getWebContents();
    if (!menuInfo.misspelledWord || menuInfo.misspelledWord.length < 1) {
      return menu;
    }

    // Ensure that we have a spell-checker for this language
    if (!this.spellCheckHandler.currentSpellchecker) {
      return menu;
    }

    // Ensure that we have valid corrections for that word
    let corrections = await this.spellCheckHandler.getCorrectionsForMisspelling(menuInfo.misspelledWord);

    if (corrections && corrections.length) {
      corrections.forEach((correction) => {
        let item = new MenuItem({
          label: correction,
          click: () => target.replaceMisspelling(correction)
        });

        menu.append(item);
      });

      this.addSeparator(menu);
    }

    // Gate learning words based on OS support. At some point we can manage a
    // custom dictionary for Hunspell, but today is not that day
    if (process.platform === 'darwin') {
      let learnWord = new MenuItem({
        label: this.stringTable.addToDictionary(),
        click: async () => {
          // NB: This is a gross fix to invalidate the spelling underline,
          // refer to https://github.com/tinyspeck/slack-winssb/issues/354
          target.replaceMisspelling(menuInfo.selectionText);

          try {
            await this.spellCheckHandler.addToDictionary(menuInfo.misspelledWord);
          } catch (e) {
            d(`Failed to add entry to dictionary: ${e.message}`);
          }
        }
      });

      menu.append(learnWord);
    }

    return menu;
  }

  /**
   * Adds search-related menu items.
   */
  addSearchItems(menu, menuInfo) {
    if (!menuInfo.selectionText || menuInfo.selectionText.length < 1) {
      return menu;
    }

    let match = matchesWord(menuInfo.selectionText);
    if (!match || match.length === 0) {
      return menu;
    }

    if (process.platform === 'darwin') {
      let target = this.getWebContents();

      let lookUpDefinition = new MenuItem({
        label: this.stringTable.lookUpDefinition({word: truncateString(menuInfo.selectionText)}),
        click: () => target.showDefinitionForSelection()
      });

      menu.append(lookUpDefinition);
    }

    let search = new MenuItem({
      label: this.stringTable.searchGoogle(),
      click: () => {
        let url = `https://www.google.com/#q=${encodeURIComponent(menuInfo.selectionText)}`;

        //d(`Searching Google using ${url}`);
        shell.openExternal(url);
      }
    });

    menu.append(search);
    this.addSeparator(menu);

    return menu;
  }

  isSrcUrlValid(menuInfo) {
    return menuInfo.srcURL && menuInfo.srcURL.length > 0;
  }

  /**
   * Adds "Copy Image" and "Copy Image URL" items when `src` is valid.
   */
  addImageItems(menu, menuInfo) {
    let copyImage = new MenuItem({
      label: this.stringTable.copyImage(),
      click: () => this.convertImageToBase64(menuInfo.srcURL,
        (dataURL) => clipboard.writeImage(nativeImage.createFromDataURL(dataURL)))
    });

    menu.append(copyImage);

    let copyImageUrl = new MenuItem({
      label: this.stringTable.copyImageUrl(),
      click: () => clipboard.writeText(menuInfo.srcURL)
    });

    menu.append(copyImageUrl);
    return menu;
  }

  /**
   * Adds the Cut menu item
   */
  addCut(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.cut(),
      accelerator: 'CommandOrControl+X',
      enabled: menuInfo.editFlags.canCut,
      click: () => target.cut()
    }));

    return menu;
  }

  /**
   * Adds the Copy menu item.
   */
  addCopy(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.copy(),
      accelerator: 'CommandOrControl+C',
      enabled: menuInfo.editFlags.canCopy,
      click: () => target.copy()
    }));

    return menu;
  }

  /**
   * Adds the Paste menu item.
   */
  addPaste(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.paste(),
      accelerator: 'CommandOrControl+V',
      enabled: menuInfo.editFlags.canPaste,
      click: () => target.paste()
    }));

    return menu;
  }

  /**
   * Adds a separator item.
   */
  addSeparator(menu) {
    menu.append(new MenuItem({type: 'separator'}));
    return menu;
  }

  /**
   * Adds the "Inspect Element" menu item.
   */
  addInspectElement(menu, menuInfo, needsSeparator=true) {
    let target = this.getWebContents();
    if (!this.debugMode) return menu;
    if (needsSeparator) this.addSeparator(menu);

    let inspect = new MenuItem({
      label: this.stringTable.inspectElement(),
      click: () => target.inspectElement(menuInfo.x, menuInfo.y)
    });

    menu.append(inspect);
    return menu;
  }

  /**
   * Converts an image to a base-64 encoded string.
   *
   * @param  {String} url           The image URL
   * @param  {Function} callback    A callback that will be invoked with the result
   * @param  {String} outputFormat  The image format to use, defaults to 'image/png'
   */
  convertImageToBase64(url, callback, outputFormat='image/png') {
    let canvas = document.createElement('CANVAS');
    let ctx = canvas.getContext('2d');
    let img = new Image();
    img.crossOrigin = 'Anonymous';

    img.onload = () => {
      canvas.height = img.height;
      canvas.width = img.width;
      ctx.drawImage(img, 0, 0);

      let dataURL = canvas.toDataURL(outputFormat);
      canvas = null;
      callback(dataURL);
    };

    img.src = url;
  }
}
<commit_msg>no need to make remote.getCurrentWindow() call now that electron defaults to the focused BrowserWindow<commit_after>import {clipboard, nativeImage, remote, shell} from 'electron';
import {truncateString, matchesWord} from './utility';

const {Menu, MenuItem} = remote;

let d = require('debug')('electron-spellchecker:context-menu-builder');

const contextMenuStringTable = {
  copyMail: () => `Copy Email Address`,
  copyLinkUrl: () => `Copy Link`,
  openLinkUrl: () => `Open Link`,
  copyImageUrl: () => `Copy Image URL`,
  copyImage: () => `Copy Image`,
  addToDictionary: () => `Add to Dictionary`,
  lookUpDefinition: ({word}) => `Look Up "${word}"`,
  searchGoogle: () => `Search with Google`,
  cut: () => `Cut`,
  copy: () => `Copy`,
  paste: () => `Paste`,
  inspectElement: () => `Inspect Element`,
};

/**
 * ContextMenuBuilder creates context menus based on the content clicked - this
 * information is derived from
 * https://github.com/electron/electron/blob/master/docs/api/web-contents.md#event-context-menu,
 * which we use to generate the menu. We also use the spell-check information to
 * generate suggestions.
 */
export default class ContextMenuBuilder {
  /**
   * Creates an instance of ContextMenuBuilder
   *
   * @param  {SpellCheckHandler} spellCheckHandler  The spell checker to generate
   *                                                recommendations for.
   * @param  {BrowserWindow|WebView} windowOrWebView  The hosting window/WebView
   * @param  {Boolean} debugMode    If true, display the "Inspect Element" menu item.
   * @param  {function} processMenu If passed, this method will be passed the menu to change
   *                                it prior to display. Signature: (menu, info) => menu
   */
  constructor(spellCheckHandler, windowOrWebView=null, debugMode=false, processMenu=(m) => m) {
    this.spellCheckHandler = spellCheckHandler;
    this.debugMode = debugMode;
    this.processMenu = processMenu;
    this.menu = null;
    this.stringTable = Object.assign({}, contextMenuStringTable);

    windowOrWebView = windowOrWebView || remote.getCurrentWebContents();

    let ctorName = Object.getPrototypeOf(windowOrWebView).constructor.name;
    if (ctorName === 'WebContents') {
      this.getWebContents = () => windowOrWebView;
    } else {
      // NB: We do this because at the time a WebView is created, it doesn't
      // have a WebContents, we need to defer the call to getWebContents
      this.getWebContents = 'webContents' in windowOrWebView ?
        () => windowOrWebView.webContents :
        () => windowOrWebView.getWebContents();
    }
  }

  /**
   * Override the default logger for this class. You probably want to use
   * {{setGlobalLogger}} instead
   *
   * @param {Function} fn   The function which will operate like console.log
   */
  static setLogger(fn) {
    d = fn;
  }

  /**
   * Specify alternate string formatter for each context menu.
   * String table consist of string formatter as function instead per each context menu item,
   * allows to change string in runtime. All formatters are simply typeof () => string, except
   * lookUpDefinition provides word, ({word}) => string.
   *
   * @param {Object} stringTable The object contains string foramtter function for context menu.
   * It is allowed to specify only certain menu string as necessary, which will makes other string
   * fall backs to default.
   *
   */
  setAlternateStringFormatter(stringTable) {
    this.stringTable = Object.assign(this.stringTable, stringTable);
  }

  /**
   * Shows a popup menu given the information returned from the context-menu
   * event. This is probably the only method you need to call in this class.
   *
   * @param  {Object} contextInfo   The object returned from the 'context-menu'
   *                                Electron event.
   *
   * @return {Promise}              Completion
   */
  async showPopupMenu(contextInfo) {
    let menu = await this.buildMenuForElement(contextInfo);
    if (!menu) return;
    menu.popup();
  }

  /**
   * Builds a context menu specific to the given info that _would_ be shown
   * immediately by {{showPopupMenu}}. Use this to add your own menu items to
   * the list but use most of the default behavior.
   *
   * @return {Promise<Menu>}      The newly created `Menu`
   */
  async buildMenuForElement(info) {
    //d(`Got context menu event with args: ${JSON.stringify(info)}`);

    if (info.linkURL && info.linkURL.length > 0) {
      return this.buildMenuForLink(info);
    }

    if (info.hasImageContents && info.srcURL && info.srcURL.length > 1) {
      return this.buildMenuForImage(info);
    }

    if (info.isEditable || (info.inputFieldType && info.inputFieldType !== 'none')) {
      return await this.buildMenuForTextInput(info);
    }

    return this.buildMenuForText(info);
  }

  /**
   * Builds a menu applicable to a text input field.
   *
   * @return {Menu}  The `Menu`
   */
  async buildMenuForTextInput(menuInfo) {
    let menu = new Menu();

    await this.addSpellingItems(menu, menuInfo);
    this.addSearchItems(menu, menuInfo);

    this.addCut(menu, menuInfo);
    this.addCopy(menu, menuInfo);
    this.addPaste(menu, menuInfo);
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to a link element.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForLink(menuInfo) {
    let menu = new Menu();
    let isEmailAddress = menuInfo.linkURL.startsWith('mailto:');

    let copyLink = new MenuItem({
      label: isEmailAddress ? this.stringTable.copyMail() : this.stringTable.copyLinkUrl(),
      click: () => {
        // Omit the mailto: portion of the link; we just want the address
        clipboard.writeText(isEmailAddress ?
          menuInfo.linkText : menuInfo.linkURL);
      }
    });

    let openLink = new MenuItem({
      label: this.stringTable.openLinkUrl(),
      click: () => {
        //d(`Navigating to: ${menuInfo.linkURL}`);
        shell.openExternal(menuInfo.linkURL);
      }
    });

    menu.append(copyLink);
    menu.append(openLink);

    if (this.isSrcUrlValid(menuInfo)) {
      this.addSeparator(menu);
      this.addImageItems(menu, menuInfo);
    }

    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to a text field.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForText(menuInfo) {
    let menu = new Menu();

    this.addSearchItems(menu, menuInfo);
    this.addCopy(menu, menuInfo);
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Builds a menu applicable to an image.
   *
   * @return {Menu}  The `Menu`
   */
  buildMenuForImage(menuInfo) {
    let menu = new Menu();

    if (this.isSrcUrlValid(menuInfo)) {
      this.addImageItems(menu, menuInfo);
    }
    this.addInspectElement(menu, menuInfo);
    this.processMenu(menu, menuInfo);

    return menu;
  }

  /**
   * Checks if the current text selection contains a single misspelled word and
   * if so, adds suggested spellings as individual menu items.
   */
  async addSpellingItems(menu, menuInfo) {
    let target = this.getWebContents();
    if (!menuInfo.misspelledWord || menuInfo.misspelledWord.length < 1) {
      return menu;
    }

    // Ensure that we have a spell-checker for this language
    if (!this.spellCheckHandler.currentSpellchecker) {
      return menu;
    }

    // Ensure that we have valid corrections for that word
    let corrections = await this.spellCheckHandler.getCorrectionsForMisspelling(menuInfo.misspelledWord);

    if (corrections && corrections.length) {
      corrections.forEach((correction) => {
        let item = new MenuItem({
          label: correction,
          click: () => target.replaceMisspelling(correction)
        });

        menu.append(item);
      });

      this.addSeparator(menu);
    }

    // Gate learning words based on OS support. At some point we can manage a
    // custom dictionary for Hunspell, but today is not that day
    if (process.platform === 'darwin') {
      let learnWord = new MenuItem({
        label: this.stringTable.addToDictionary(),
        click: async () => {
          // NB: This is a gross fix to invalidate the spelling underline,
          // refer to https://github.com/tinyspeck/slack-winssb/issues/354
          target.replaceMisspelling(menuInfo.selectionText);

          try {
            await this.spellCheckHandler.addToDictionary(menuInfo.misspelledWord);
          } catch (e) {
            d(`Failed to add entry to dictionary: ${e.message}`);
          }
        }
      });

      menu.append(learnWord);
    }

    return menu;
  }

  /**
   * Adds search-related menu items.
   */
  addSearchItems(menu, menuInfo) {
    if (!menuInfo.selectionText || menuInfo.selectionText.length < 1) {
      return menu;
    }

    let match = matchesWord(menuInfo.selectionText);
    if (!match || match.length === 0) {
      return menu;
    }

    if (process.platform === 'darwin') {
      let target = this.getWebContents();

      let lookUpDefinition = new MenuItem({
        label: this.stringTable.lookUpDefinition({word: truncateString(menuInfo.selectionText)}),
        click: () => target.showDefinitionForSelection()
      });

      menu.append(lookUpDefinition);
    }

    let search = new MenuItem({
      label: this.stringTable.searchGoogle(),
      click: () => {
        let url = `https://www.google.com/#q=${encodeURIComponent(menuInfo.selectionText)}`;

        //d(`Searching Google using ${url}`);
        shell.openExternal(url);
      }
    });

    menu.append(search);
    this.addSeparator(menu);

    return menu;
  }

  isSrcUrlValid(menuInfo) {
    return menuInfo.srcURL && menuInfo.srcURL.length > 0;
  }

  /**
   * Adds "Copy Image" and "Copy Image URL" items when `src` is valid.
   */
  addImageItems(menu, menuInfo) {
    let copyImage = new MenuItem({
      label: this.stringTable.copyImage(),
      click: () => this.convertImageToBase64(menuInfo.srcURL,
        (dataURL) => clipboard.writeImage(nativeImage.createFromDataURL(dataURL)))
    });

    menu.append(copyImage);

    let copyImageUrl = new MenuItem({
      label: this.stringTable.copyImageUrl(),
      click: () => clipboard.writeText(menuInfo.srcURL)
    });

    menu.append(copyImageUrl);
    return menu;
  }

  /**
   * Adds the Cut menu item
   */
  addCut(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.cut(),
      accelerator: 'CommandOrControl+X',
      enabled: menuInfo.editFlags.canCut,
      click: () => target.cut()
    }));

    return menu;
  }

  /**
   * Adds the Copy menu item.
   */
  addCopy(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.copy(),
      accelerator: 'CommandOrControl+C',
      enabled: menuInfo.editFlags.canCopy,
      click: () => target.copy()
    }));

    return menu;
  }

  /**
   * Adds the Paste menu item.
   */
  addPaste(menu, menuInfo) {
    let target = this.getWebContents();
    menu.append(new MenuItem({
      label: this.stringTable.paste(),
      accelerator: 'CommandOrControl+V',
      enabled: menuInfo.editFlags.canPaste,
      click: () => target.paste()
    }));

    return menu;
  }

  /**
   * Adds a separator item.
   */
  addSeparator(menu) {
    menu.append(new MenuItem({type: 'separator'}));
    return menu;
  }

  /**
   * Adds the "Inspect Element" menu item.
   */
  addInspectElement(menu, menuInfo, needsSeparator=true) {
    let target = this.getWebContents();
    if (!this.debugMode) return menu;
    if (needsSeparator) this.addSeparator(menu);

    let inspect = new MenuItem({
      label: this.stringTable.inspectElement(),
      click: () => target.inspectElement(menuInfo.x, menuInfo.y)
    });

    menu.append(inspect);
    return menu;
  }

  /**
   * Converts an image to a base-64 encoded string.
   *
   * @param  {String} url           The image URL
   * @param  {Function} callback    A callback that will be invoked with the result
   * @param  {String} outputFormat  The image format to use, defaults to 'image/png'
   */
  convertImageToBase64(url, callback, outputFormat='image/png') {
    let canvas = document.createElement('CANVAS');
    let ctx = canvas.getContext('2d');
    let img = new Image();
    img.crossOrigin = 'Anonymous';

    img.onload = () => {
      canvas.height = img.height;
      canvas.width = img.width;
      ctx.drawImage(img, 0, 0);

      let dataURL = canvas.toDataURL(outputFormat);
      canvas = null;
      callback(dataURL);
    };

    img.src = url;
  }
}
<|endoftext|>


<commit_before>//var logger = require("loggero").create("controllers/resolver");
var inherit    = require("../inherit");
var Module     = require("../module");
var Controller = require("../Controller");

function Resolver(context) {
  Controller.call(this, context);
}

inherit.base(Resolver).extends(Controller);

Resolver.prototype.resolve = function(name, referrer) {
  var moduleMeta = new Module.Meta({
    name: name,
    referrer: referrer
  });

  return this.context.services.resolve.run(moduleMeta).then(configureId);
};

function configureId(moduleMeta) {
  var result = {};

  if (!moduleMeta.path && moduleMeta.url) {
    result.path = moduleMeta.url && moduleMeta.url.href;
  }

  if (!moduleMeta.hasOwnProperty("id") && moduleMeta.path) {
    result.id = moduleMeta.path;
  }

  return moduleMeta.configure(result);
};

module.exports = Resolver;
<commit_msg>Fixed casing that broke the build<commit_after>//var logger = require("loggero").create("controllers/resolver");
var inherit    = require("../inherit");
var Module     = require("../module");
var Controller = require("../controller");

function Resolver(context) {
  Controller.call(this, context);
}

inherit.base(Resolver).extends(Controller);

Resolver.prototype.resolve = function(name, referrer) {
  var moduleMeta = new Module.Meta({
    name: name,
    referrer: referrer
  });

  return this.context.services.resolve.run(moduleMeta).then(configureId);
};

function configureId(moduleMeta) {
  var result = {};

  if (!moduleMeta.path && moduleMeta.url) {
    result.path = moduleMeta.url && moduleMeta.url.href;
  }

  if (!moduleMeta.hasOwnProperty("id") && moduleMeta.path) {
    result.id = moduleMeta.path;
  }

  return moduleMeta.configure(result);
};

module.exports = Resolver;
<|endoftext|>


<commit_before>/*!
 * OS.js - JavaScript Operating System
 *
 * Copyright (c) 2011-2015, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */
(function(Service, Window, Utils, API, VFS, GUI) {
  'use strict';

  /////////////////////////////////////////////////////////////////////////////
  // SERVICE
  /////////////////////////////////////////////////////////////////////////////

  function ArduinoService(args, metadata) {
    Service.apply(this, ['ArduinoService', args, metadata]);

    this.pollingNetwork = false;
    this.pollingInterval = null;
    this.cache = {
      devices: {}
    };
  }

  ArduinoService.prototype = Object.create(Service.prototype);
  ArduinoService.constructor = Service;

  ArduinoService.prototype.destroy = function() {
    var wm = OSjs.Core.getWindowManager();
    if ( wm ) {
      wm.destroyNotificationIcon('_ArduinoNotification');
      wm.destroyNotificationIcon('_ArduinoNetworkNotification');
      wm.destroyNotificationIcon('_ArduinoWIFINotification');
    }

    this.cache = {};
    this.pollingInterval = clearInterval(this._pollingInteval);

    return Service.prototype.destroy.apply(this, arguments);
  };

  ArduinoService.prototype.init = function(settings, metadata, onInited) {
    Service.prototype.init.apply(this, arguments);

    var self = this;
    var wm = OSjs.Core.getWindowManager();

    function showNetworkContextMenu(ev) {
      var mnu = [];
      var devs = self.cache.devices;

      function createSubItem(d, idx) {
        if ( idx > 0 ) {
          mnu.push({title: '
', titleHTML: true});
        }

        mnu.push({
          title: '' + d + '',
          titleHTML: true
        });

        Object.keys(devs[d]).forEach(function(i) {
          mnu.push({
            title: i + ': ' + devs[d][i]
          });
        });
      }

      Object.keys(devs).forEach(function(d, idx) {
        createSubItem(d, idx);
      });

      OSjs.API.createMenu(mnu, ev);
    }

    function showWIFIContextMenu(ev) {
      self.externalCall('iwinfo', {}, function(err, result) {
        var info = (result || '').split(' ');
        var keys = ['ap', 'ssid', 'security', 'signal'];
        var sts = info[0] !== '00:00:00:00:00:00' ? 'connected' : 'disconnected';
        var menuItems = [
          {titleHTML: true, title: 'Status: ' + sts}
        ];

        keys.forEach(function(key, idx) {
          var val = info[idx] || null;
          menuItems.push({
            titleHTML: true,
            title: Utils.format('{0}: {1}', key, Utils.$escape(String(val)))
          });
        });

        OSjs.API.createMenu(menuItems, ev);
      });

    }

    function showContextMenu(ev) {
      var mnu = [
        {title: 'Open Settings', onClick: function(ev) {
          API.launch('ApplicationArduinoSettings');
        }}
      ];
      OSjs.API.createMenu(mnu, ev);
    }

    wm.createNotificationIcon('_ArduinoNotification', {
      image: API.getIcon('devices/audio-card.png'),
      title: 'Arduino Device',
      onContextMenu: showContextMenu,
      onClick: showContextMenu
    });

    wm.createNotificationIcon('_ArduinoNetworkNotification', {
      image: API.getIcon('devices/network-wired.png'),
      title: 'Arduino Network Devices',
      onContextMenu: showNetworkContextMenu,
      onClick: showNetworkContextMenu
    });
    wm.createNotificationIcon('_ArduinoWIFINotification', {
      image: API.getIcon('devices/network-wireless.png'),
      title: 'Open Settings',
      onContextMenu: showWIFIContextMenu,
      onClick: showWIFIContextMenu
    });

    this.pollingInterval = setInterval(function() {
      self.pollNetwork();
    }, 15000);

    this.pollNetwork();

    onInited();
  };

  ArduinoService.prototype._onMessage = function(obj, msg, args) {
    if ( msg === 'attention' ) {
      // args.foo
    }
  };

  ArduinoService.prototype.pollNetwork = function() {
    var self = this;

    this.pollingNetwork = true;

    this.cache.devices = {};

    function getArpTable(table, dev) {
      var result = null;
      table.forEach(function(iter) {
        if ( iter.Device === dev ) {
          result = iter;
        }
        return !!result;
      });
      return result;
    };

    this.externalCall('netinfo', {}, function(err, result) {
      var devs = [];
      try {
        devs = Object.keys(result.deviceinfo);
      } catch ( e ) {
        console.warn('Error parsing devices', e);
      }

      devs.forEach(function(dev) {
        var arp =  getArpTable(result.arptable, dev) || {};
        var details = {
          'IP': arp['IP address'] || '',
          'Mask': arp['Mask'] || '',
          'MAC': arp['HW address'] || ''
        };
        self.cache.devices[dev] = details;
      });
    });
  };

  ArduinoService.prototype.externalCall = function(fn, args, cb) {
    this._call(fn, args, function(response) {
      response = response || {};
      if ( response.result ) {
        cb(false, response.result);
      } else {
        cb(response.error || 'No response from device');
      }
    }, function(err) {
      cb('Failed to get response from device: ' + err);
    }, false);
  };

  /////////////////////////////////////////////////////////////////////////////
  // EXPORTS
  /////////////////////////////////////////////////////////////////////////////

  OSjs.Applications = OSjs.Applications || {};
  OSjs.Applications.ArduinoService = OSjs.Applications.ArduinoService || {};
  OSjs.Applications.ArduinoService.Class = ArduinoService;

})(OSjs.Core.Service, OSjs.Core.Window, OSjs.Utils, OSjs.API, OSjs.VFS, OSjs.GUI);
<commit_msg>Better network stats handling in arduino service (#169)<commit_after>/*!
 * OS.js - JavaScript Operating System
 *
 * Copyright (c) 2011-2015, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */
(function(Service, Window, Utils, API, VFS, GUI) {
  'use strict';

  /////////////////////////////////////////////////////////////////////////////
  // SERVICE
  /////////////////////////////////////////////////////////////////////////////

  function ArduinoService(args, metadata) {
    Service.apply(this, ['ArduinoService', args, metadata]);

    this.busy = false;
  }

  ArduinoService.prototype = Object.create(Service.prototype);
  ArduinoService.constructor = Service;

  ArduinoService.prototype.destroy = function() {
    var wm = OSjs.Core.getWindowManager();
    if ( wm ) {
      wm.destroyNotificationIcon('_ArduinoNotification');
      wm.destroyNotificationIcon('_ArduinoNetworkNotification');
      wm.destroyNotificationIcon('_ArduinoWIFINotification');
    }

    return Service.prototype.destroy.apply(this, arguments);
  };

  ArduinoService.prototype.init = function(settings, metadata, onInited) {
    Service.prototype.init.apply(this, arguments);

    var self = this;
    var wm = OSjs.Core.getWindowManager();

    function showNetworkContextMenu(ev) {
      var mnu = [];

      function createSubItem(devs, d, idx) {
        if ( idx > 0 ) {
          mnu.push({title: '
', titleHTML: true});
        }

        mnu.push({
          title: '' + d + '',
          titleHTML: true
        });

        Object.keys(devs[d]).forEach(function(i) {
          mnu.push({
            title: i + ': ' + devs[d][i]
          });
        });
      }

      self.pollNetwork(function(devs) {
        Object.keys(devs).forEach(function(d, idx) {
          createSubItem(devs, d, idx);
        });
        OSjs.API.createMenu(mnu, ev);
      });
    }

    function showWIFIContextMenu(ev) {
      self.pollWireless(function(result) {
        var menuItems = [];
        Object.keys(result).forEach(function(k) {
          var val = result[k];
          menuItems.push({
            titleHTML: true,
            title: Utils.format('{0}: {1}', k, Utils.$escape(String(val)))
          });
        });
        OSjs.API.createMenu(menuItems, ev);
      });
    }

    function showContextMenu(ev) {
      var mnu = [
        {title: 'Open Settings', onClick: function(ev) {
          API.launch('ApplicationArduinoSettings');
        }}
      ];
      OSjs.API.createMenu(mnu, ev);
    }

    wm.createNotificationIcon('_ArduinoNotification', {
      image: API.getIcon('devices/audio-card.png'),
      title: 'Arduino Device',
      onContextMenu: showContextMenu,
      onClick: showContextMenu
    });

    wm.createNotificationIcon('_ArduinoNetworkNotification', {
      image: API.getIcon('devices/network-wired.png'),
      title: 'Arduino Network Devices',
      onContextMenu: showNetworkContextMenu,
      onClick: showNetworkContextMenu
    });
    wm.createNotificationIcon('_ArduinoWIFINotification', {
      image: API.getIcon('devices/network-wireless.png'),
      title: 'Open Settings',
      onContextMenu: showWIFIContextMenu,
      onClick: showWIFIContextMenu
    });

    onInited();
  };

  ArduinoService.prototype._onMessage = function(obj, msg, args) {
    if ( msg === 'attention' ) {
      // args.foo
    }
  };

  ArduinoService.prototype.pollWireless = function(cb) {
    var self = this;
    if ( this.busy ) {
      return;
    }
    this.busy = true;

    function isConnected(tst) {
      tst = tst || '00:00:00:00:00:00';
      return tst !== '00:00:00:00:00:00' ? 'connected' : 'disconnected'
    }

    this.externalCall('iwinfo', {}, function(err, result) {
      var info = (result || '').split(' ');
      var keys = ['ap', 'ssid', 'security', 'signal'];
      var list = {
        'status': isConnected(info[0])
      };

      keys.forEach(function(key, idx) {
        list[key] = info[idx] || null;
      });

      self.busy = false;

      cb(list);
    });
  };

  ArduinoService.prototype.pollNetwork = function(cb) {
    var self = this;

    if ( this.busy ) {
      return;
    }
    this.busy = true;

    function getArpTable(table, dev) {
      var result = null;
      table.forEach(function(iter) {
        if ( iter.Device === dev ) {
          result = iter;
        }
        return !!result;
      });
      return result;
    };

    this.externalCall('netinfo', {}, function(err, result) {
      var devs = [];
      try {
        devs = Object.keys(result.deviceinfo);
      } catch ( e ) {
        console.warn('Error parsing devices', e);
      }

      var list = {};
      devs.forEach(function(dev) {
        var arp =  getArpTable(result.arptable, dev) || {};
        var details = {
          'IP': arp['IP address'] || '',
          'Mask': arp['Mask'] || '',
          'MAC': arp['HW address'] || ''
        };
        list[dev] = details;
      });

      self.busy = false;

      cb(list);
    });
  };

  ArduinoService.prototype.externalCall = function(fn, args, cb) {
    this._call(fn, args, function(response) {
      response = response || {};
      if ( response.result ) {
        cb(false, response.result);
      } else {
        cb(response.error || 'No response from device');
      }
    }, function(err) {
      cb('Failed to get response from device: ' + err);
    }, false);
  };

  /////////////////////////////////////////////////////////////////////////////
  // EXPORTS
  /////////////////////////////////////////////////////////////////////////////

  OSjs.Applications = OSjs.Applications || {};
  OSjs.Applications.ArduinoService = OSjs.Applications.ArduinoService || {};
  OSjs.Applications.ArduinoService.Class = ArduinoService;

})(OSjs.Core.Service, OSjs.Core.Window, OSjs.Utils, OSjs.API, OSjs.VFS, OSjs.GUI);
<|endoftext|>


<commit_before>// Ensure Common Expressions are present
var MongoClient = require('mongodb').MongoClient;
var async = require('async');
var path = require('path');
var fs = require('fs');

exports.task = function(app, next) {

	MongoClient.connect('mongodb://127.0.0.1:27017/regbin', function(err, db) {
	    if (err) throw err;

	    var collection = db.collection('common');

	    var commonDataPath = path.join(__rootDir, 'lib', 'commonRegex.json');
	    fs.readFile(commonDataPath, { encoding: 'utf-8' }, function(err, data) {
	        if (err) throw err;

	        var commonExpressions = JSON.parse(data.toString());

	        async.each(commonExpressions,
	            function saveToDatabase(expression, done) {

	                collection.findOne({ name: expression.name }, function(err, document) {
	                    if (err) throw err;

	                    if (document) {
	                        done();
	                    }
	                    else {
	                        collection.insert(expression, function(err, docs) {
	                            if (err) throw err;
	                            done();
	                        });
	                    }
	                });
	            },
	            function finished(err) {
	                next();
	            });
	    });
	});
};<commit_msg>removed: disabled DB code for now.<commit_after>// Ensure Common Expressions are present
var MongoClient = require('mongodb').MongoClient;
var async = require('async');
var path = require('path');
var fs = require('fs');

exports.task = function(app, next) {

/*
	MongoClient.connect('mongodb://127.0.0.1:27017/regbin', function(err, db) {
	    if (err) throw err;

	    var collection = db.collection('common');

	    var commonDataPath = path.join(__rootDir, 'lib', 'commonRegex.json');
	    fs.readFile(commonDataPath, { encoding: 'utf-8' }, function(err, data) {
	        if (err) throw err;

	        var commonExpressions = JSON.parse(data.toString());

	        async.each(commonExpressions,
	            function saveToDatabase(expression, done) {

	                collection.findOne({ name: expression.name }, function(err, document) {
	                    if (err) throw err;

	                    if (document) {
	                        done();
	                    }
	                    else {
	                        collection.insert(expression, function(err, docs) {
	                            if (err) throw err;
	                            done();
	                        });
	                    }
	                });
	            },
	            function finished(err) {
	                next();
	            });
	    });
	});
*/

    process.nextTick(next);
};<|endoftext|>


<commit_before>// namespace
var App = App || {};

App.PlayMissionState = (function () {
    "use strict";

    var fn = function (game) {
        Phaser.State.call(this, game);
    };

    fn.prototype = Object.create(Phaser.State.prototype);
    fn.prototype.constructor = fn;

    fn.prototype.init = function () {
        this.sector = {
            name: "Test Sector",
            width: this.game.world.width * 2,
            height: this.game.world.height * 2
        };
    };

    fn.prototype.preload = function () {
        this.load.image('space', 'assets/images/spaceBGDarkPurple.png');
        this.load.image('player', 'assets/images/playerShipO.png');
    };

    fn.prototype.create = function () {
        // background
        this.background = this.add.tileSprite(0, 0, this.sector.width, this.sector.height, 'space');
        this.game.world.setBounds(0, 0, this.sector.width, this.sector.height);

        // use p2 for ships
        this.game.physics.startSystem(Phaser.Physics.P2JS);

        this.player = this.add.sprite(this.game.world.width / 2, this.game.world.height / 2, 'player');
        this.player.anchor.setTo(0.5);
        this.player.scale.setTo(0.5);

        this.game.physics.p2.enable(this.player);

        this.cursors = game.input.keyboard.createCursorKeys();

        //  Notice that the sprite doesn't have any momentum at all,
        //  it's all just set by the camera follow type.
        //  0.1 is the amount of linear interpolation to use.
        //  The smaller the value, the smooth the camera (and the longer it takes to catch up)
        game.camera.follow(this.player, Phaser.Camera.FOLLOW_LOCKON, 0.1, 0.1);
    };

    fn.prototype.update = function () {
        this.player.body.setZeroVelocity();

        if (this.cursors.up.isDown) {
            this.player.body.moveForward(300);
        }
        else if (this.cursors.down.isDown) {
            this.player.body.moveBackward(300);
        }

        if (this.cursors.left.isDown) {
            this.player.body.rotateLeft(100);
        }
        else if (this.cursors.right.isDown) {
            this.player.body.rotateRight(100);
        }

        if (this.cursors.right.isUp && this.cursors.left.isUp) {
            this.player.body.rotateRight(0);
            this.player.body.rotateLeft(0);
        }
    }

    return fn;
})();
<commit_msg>Make the ship move how we want it to<commit_after>// namespace
var App = App || {};

App.PlayMissionState = (function () {
    "use strict";

    var fn = function (game) {
        Phaser.State.call(this, game);
    };

    fn.prototype = Object.create(Phaser.State.prototype);
    fn.prototype.constructor = fn;

    fn.prototype.init = function () {
        this.sector = {
            name: "Test Sector",
            width: this.game.world.width * 2,
            height: this.game.world.height * 2
        };
    };

    fn.prototype.preload = function () {
        this.load.image('space', 'assets/images/spaceBGDarkPurple.png');
        this.load.image('player', 'assets/images/playerShipO.png');
    };

    fn.prototype.create = function () {
        // background
        this.background = this.add.tileSprite(0, 0, this.sector.width, this.sector.height, 'space');
        this.game.world.setBounds(0, 0, this.sector.width, this.sector.height);

        // use p2 for ships
        this.game.physics.startSystem(Phaser.Physics.P2JS);

        this.player = this.add.sprite(this.game.world.width / 2, this.game.world.height / 2, 'player');
        this.player.anchor.setTo(0.5);
        this.player.scale.setTo(0.5);

        this.game.physics.p2.enable(this.player);

        this.cursors = game.input.keyboard.createCursorKeys();

        //  Notice that the sprite doesn't have any momentum at all,
        //  it's all just set by the camera follow type.
        //  0.1 is the amount of linear interpolation to use.
        //  The smaller the value, the smooth the camera (and the longer it takes to catch up)
        game.camera.follow(this.player, Phaser.Camera.FOLLOW_LOCKON, 0.1, 0.1);
    };

    fn.prototype.update = function () {

        if (this.cursors.up.isDown) {
            this.player.body.thrust(300);
        }
        else if (this.cursors.down.isDown) {
            this.player.body.reverse(300);
        }
        else {
            this.player.body.setZeroRotation();
        }

        if (this.cursors.left.isDown) {
            this.player.body.rotateLeft(100);
        }
        else if (this.cursors.right.isDown) {
            this.player.body.rotateRight(100);
        }

        if (this.cursors.right.isUp && this.cursors.left.isUp) {
            this.player.body.rotateRight(0);
            this.player.body.rotateLeft(0);
        }
    }

    return fn;
})();
<|endoftext|>


<commit_before>const ValidationError = require('../ValidationError')
const genericValidator = require('./genericValidator')

const presence = (expected, value, message) => {
  if (expected !== 'required') {
    return true
  }

  if (typeof value === 'undefined' || Object.keys(value).length === 0) {
    return new ValidationError(message || 'Required')
  }

  return true
}

const reference = (unused, value, message) => {
  if (!value) {
    return true
  }

  if (typeof value._ref !== 'string') {
    return new ValidationError(message || 'Must be a reference to a document', {paths: ['$']})
  }

  return true
}

const assetRequired = (flag, value, message) => {
  if (!value || !value.asset || !value.asset._ref) {
    const assetType = flag.assetType || 'Asset'
    return new ValidationError(message || `${assetType} required`)
  }

  return true
}

module.exports = Object.assign({}, genericValidator, {
  presence,
  reference,
  assetRequired
})
<commit_msg>[validation] Exclude meta fields from object emptiness check (#1282)<commit_after>const ValidationError = require('../ValidationError')
const genericValidator = require('./genericValidator')

const metaKeys = ['_key', '_type', '_weak']

const presence = (expected, value, message) => {
  if (expected !== 'required') {
    return true
  }

  const keys = value && Object.keys(value).filter(key => !metaKeys.includes(key))
  if (typeof value === 'undefined' || (keys && keys.length === 0)) {
    return new ValidationError(message || 'Required')
  }

  return true
}

const reference = (unused, value, message) => {
  if (!value) {
    return true
  }

  if (typeof value._ref !== 'string') {
    return new ValidationError(message || 'Must be a reference to a document', {paths: ['$']})
  }

  return true
}

const assetRequired = (flag, value, message) => {
  if (!value || !value.asset || !value.asset._ref) {
    const assetType = flag.assetType || 'Asset'
    return new ValidationError(message || `${assetType} required`)
  }

  return true
}

module.exports = Object.assign({}, genericValidator, {
  presence,
  reference,
  assetRequired
})
<|endoftext|>


<commit_before>import React from 'react'
import {EditableValue, Range} from 'tocco-ui'

import typeEditable from './typeEditable'

const getEvents = (type, formField, formName, formData, events) =>
  typeEditable[type] && typeEditable[type].getEvents
    ? typeEditable[type].getEvents({formField, formName, formData, events})
    : events

const getValue = (type, formField, modelField, formName, formData, currentValue) =>
  typeEditable[type] && typeEditable[type].getValue
    ? typeEditable[type].getValue({formField, modelField, formName, formData, currentValue})
    : currentValue

const getOptions = (type, formField, modelField, formName, formData) =>
  typeEditable[type] && typeEditable[type].getOptions
    ? typeEditable[type].getOptions({formField, modelField, formName, formData})
    : {}

export default (type, range) => (formField, modelField, formName, value, info, events, formData) => {
  const formType = formField.dataType || formField.componentType
  const options = getOptions(formType, formField, modelField, formName, formData)

  events = getEvents(formType, formField, formName, formData, events)
  value = getValue(formType, formField, modelField, formName, formData, value)

  const Component = range ? Range : EditableValue

  return (
    <Component
      type={type}
      events={events}
      value={value}
      {...info}
      options={options}
    />
  )
}
<commit_msg>refactor(app-extension): set range text<commit_after>import React from 'react'
import {EditableValue, Range} from 'tocco-ui'

import typeEditable from './typeEditable'

const getEvents = (type, formField, formName, formData, events) =>
  typeEditable[type] && typeEditable[type].getEvents
    ? typeEditable[type].getEvents({formField, formName, formData, events})
    : events

const getValue = (type, formField, modelField, formName, formData, currentValue) =>
  typeEditable[type] && typeEditable[type].getValue
    ? typeEditable[type].getValue({formField, modelField, formName, formData, currentValue})
    : currentValue

const getOptions = (type, formField, modelField, formName, formData) =>
  typeEditable[type] && typeEditable[type].getOptions
    ? typeEditable[type].getOptions({formField, modelField, formName, formData})
    : {}

export default (type, range) => (formField, modelField, formName, value, info, events, formData) => {
  const formType = formField.dataType || formField.componentType
  const options = getOptions(formType, formField, modelField, formName, formData)

  events = getEvents(formType, formField, formName, formData, events)
  value = getValue(formType, formField, modelField, formName, formData, value)

  const Component = range ? Range : EditableValue

  return (
    <Component
      type={type}
      events={events}
      value={value}
      {...info}
      options={options}
      {...(range && {
        fromText: formData.intl.formatMessage({id: 'client.component.range.from'}),
        toText: formData.intl.formatMessage({id: 'client.component.range.to'})
      })}
    />
  )
}
<|endoftext|>


<commit_before>'use strict';

var defaultLabel;
var taxVatLabel;
var taxVatInput;

var qs = function (el) {
  return document.querySelector(el);
};

function inputHandler(masks, max, event) {
  var c = event.target;
  var v = c.value.replace(/\D/g, '');
  var m = c.value.length > max ? 1 : 0;
  VMasker(c).unMask();
  VMasker(c).maskPattern(masks[m]);
  c.value = VMasker.toPattern(v, masks[m]);
}

var getLabelByCountry = function (country, defaultLabel) {
  switch (country.toLowerCase()) {
    case 'br':
      return EBANXData.brazilAllowedDocumentFields.join(' / ').toUpperCase();
    case 'co':
      return 'DNI';
    case 'cl':
      return 'RUT';
    default:
      return defaultLabel;
  }
};

var changeTaxVatLabel = function () {
  var country = this.value;
  var newLabel = getLabelByCountry(country, defaultLabel);

  OSCRequire(country);

  setTimeout(function(){taxVatInput.placeholder = '';}, 10);

  taxVatLabel.innerHTML = newLabel;
  if(taxVatInput && (country === 'BR' || country === 'CO' || country === 'CL')) {
      setTimeout(function(){taxVatInput.placeholder = newLabel;}, 10)
    }

  if (country === 'BR' && taxVatInput) {
    var taxVatMask = newLabel.indexOf('CNPJ') !== -1 ? ['999.999.999-99', '99.999.999/9999-99'] : ['999.999.999-99', '999.999.999-99'];
    VMasker(taxVatInput).maskPattern(taxVatMask[0]);
    taxVatInput.addEventListener('input', inputHandler.bind(undefined, taxVatMask, 14), false);
  }
};

var init = function () {
  var countrySelect = qs('#billing\\:country_id');
  taxVatLabel = qs('label[for="billing\\:taxvat"]');
  taxVatInput = document.getElementById('billing:taxvat');

  if (!taxVatLabel && typeof amsCheckoutHandler() !== 'undefined') {
    taxVatLabel = amsCheckoutHandler();
  }

  if (taxVatLabel && countrySelect) {
    defaultLabel = taxVatLabel.innerHTML;

    countrySelect.addEventListener('change', changeTaxVatLabel);
    countrySelect.dispatchEvent(new Event('change'));
  }
};

var OSCRequire = function (country) {
  if (typeof ebanxRemoveOSCRequireDocument !== 'undefined') {
    ebanxRemoveOSCRequireDocument(country);
  }
};

document.addEventListener('DOMContentLoaded', init);
<commit_msg>Fix js AMS checkout verification<commit_after>'use strict';

var defaultLabel;
var taxVatLabel;
var taxVatInput;

var qs = function (el) {
  return document.querySelector(el);
};

function inputHandler(masks, max, event) {
  var c = event.target;
  var v = c.value.replace(/\D/g, '');
  var m = c.value.length > max ? 1 : 0;
  VMasker(c).unMask();
  VMasker(c).maskPattern(masks[m]);
  c.value = VMasker.toPattern(v, masks[m]);
}

var getLabelByCountry = function (country, defaultLabel) {
  switch (country.toLowerCase()) {
    case 'br':
      return EBANXData.brazilAllowedDocumentFields.join(' / ').toUpperCase();
    case 'co':
      return 'DNI';
    case 'cl':
      return 'RUT';
    default:
      return defaultLabel;
  }
};

var changeTaxVatLabel = function () {
  var country = this.value;
  var newLabel = getLabelByCountry(country, defaultLabel);

  OSCRequire(country);

  setTimeout(function(){taxVatInput.placeholder = '';}, 10);

  taxVatLabel.innerHTML = newLabel;
  if(taxVatInput && (country === 'BR' || country === 'CO' || country === 'CL')) {
      setTimeout(function(){taxVatInput.placeholder = newLabel;}, 10)
    }

  if (country === 'BR' && taxVatInput) {
    var taxVatMask = newLabel.indexOf('CNPJ') !== -1 ? ['999.999.999-99', '99.999.999/9999-99'] : ['999.999.999-99', '999.999.999-99'];
    VMasker(taxVatInput).maskPattern(taxVatMask[0]);
    taxVatInput.addEventListener('input', inputHandler.bind(undefined, taxVatMask, 14), false);
  }
};

var init = function () {
  var countrySelect = qs('#billing\\:country_id');
  taxVatLabel = qs('label[for="billing\\:taxvat"]');
  taxVatInput = document.getElementById('billing:taxvat');

  if (!taxVatLabel && typeof amsCheckoutHandler === 'function') {
    taxVatLabel = amsCheckoutHandler();
  }

  if (taxVatLabel && countrySelect) {
    defaultLabel = taxVatLabel.innerHTML;

    countrySelect.addEventListener('change', changeTaxVatLabel);
    countrySelect.dispatchEvent(new Event('change'));
  }
};

var OSCRequire = function (country) {
  if (typeof ebanxRemoveOSCRequireDocument !== 'undefined') {
    ebanxRemoveOSCRequireDocument(country);
  }
};

document.addEventListener('DOMContentLoaded', init);
<|endoftext|>


<commit_before>module.exports = {
  name: 'BetterScrollbars',
  type: 'select',
  default: 'small',
  section: 'general',
  title: 'Better scrollbars',
  description: 'Provides smaller and cleaner scrollbars across the application.',
  options: [
    { name: 'Default', value: '0' },
    { name: 'Small', value: '1' },
    { name: 'Tiny', value: '2' },
    { name: 'Off', value: '3' }
  ]
};
<commit_msg>Fixing default value.<commit_after>module.exports = {
  name: 'BetterScrollbars',
  type: 'select',
  default: '0',
  section: 'general',
  title: 'Better scrollbars',
  description: 'Provides smaller and cleaner scrollbars across the application.',
  options: [
    { name: 'Default', value: '0' },
    { name: 'Small', value: '1' },
    { name: 'Tiny', value: '2' },
    { name: 'Off', value: '3' }
  ]
};
<|endoftext|>


<commit_before>require('consoloid-framework/Consoloid/Test/UnitTest');
require('consoloid-console/Consoloid/Entity/Mentionable');
require('../Branch');
require('../Commit');

describeUnitTest('Tada.Git.Entity.Branch', function() {
  var
    branch,
    commit1,
    commit2;

  beforeEach(function(){
    commit1 = env.mock('Tada.Git.Entity.Commit');
    commit2 = env.mock('Tada.Git.Entity.Commit');
    branch = new Tada.Git.Entity.Branch({ name: 'foo', commits: [commit1, commit2] });
  });

  describe('constructor', function(){
    it('should throw when name is not present', function(){
      (function(){ new Tada.Git.Entity.Branch({ commits: [commit1 ] })}).should.throw('name must be injected');
    });
  });

  describe('getters', function(){
    it('should has for name', function(){
      branch.getName().should.be.eql('foo');
    });

    it('should has for latest commit', function(){
      branch.getLatestCommit().should.be.eql(commit1);
    });

    it('should have one for commits', function() {
      branch.getCommits().should.equal(branch.commits);
    })
  });

  describe('#setCommits(commits)', function(){
    it('should throw when array does not contains commits', function(){
      (function(){ branch.setCommits([]); }).should.throw('Branch must has at least one commit with type Tada.Git.Entity.Commit');
      (function(){ branch.setCommits(['foo']); }).should.throw('Branch must has at least one commit with type Tada.Git.Entity.Commit');
    });

    it('should set the commits', function(){
      branch.setCommits([commit2]);
      branch.getLatestCommit().should.be.eql(commit2);
    });
  });
});
<commit_msg>BranchTest: still checked for "Branch must has" instead of "Branch must have"<commit_after>require('consoloid-framework/Consoloid/Test/UnitTest');
require('consoloid-console/Consoloid/Entity/Mentionable');
require('../Branch');
require('../Commit');

describeUnitTest('Tada.Git.Entity.Branch', function() {
  var
    branch,
    commit1,
    commit2;

  beforeEach(function(){
    commit1 = env.mock('Tada.Git.Entity.Commit');
    commit2 = env.mock('Tada.Git.Entity.Commit');
    branch = new Tada.Git.Entity.Branch({ name: 'foo', commits: [commit1, commit2] });
  });

  describe('constructor', function(){
    it('should throw when name is not present', function(){
      (function(){ new Tada.Git.Entity.Branch({ commits: [commit1 ] })}).should.throw('name must be injected');
    });
  });

  describe('getters', function(){
    it('should has for name', function(){
      branch.getName().should.be.eql('foo');
    });

    it('should has for latest commit', function(){
      branch.getLatestCommit().should.be.eql(commit1);
    });

    it('should have one for commits', function() {
      branch.getCommits().should.equal(branch.commits);
    })
  });

  describe('#setCommits(commits)', function(){
    it('should throw when array does not contains commits', function(){
      (function(){ branch.setCommits([]); }).should.throw('Branch must have at least one commit with type Tada.Git.Entity.Commit');
      (function(){ branch.setCommits(['foo']); }).should.throw('Branch must have at least one commit with type Tada.Git.Entity.Commit');
    });

    it('should set the commits', function(){
      branch.setCommits([commit2]);
      branch.getLatestCommit().should.be.eql(commit2);
    });
  });
});
<|endoftext|>


<commit_before>'use strict';

var Promise = require('lie');
var util = require('util');
var path = require('path');
var ThaliPeerAction = require('../thaliPeerPool/thaliPeerAction');
var actionState = ThaliPeerAction.actionState;
var assert = require('assert');
var thaliConfig = require('../thaliConfig');
var logger = require('../../ThaliLogger')('thaliReplicationPeerAction');
var LocalSeqManager = require('./localSeqManager');
var RefreshTimerManager = require('./utilities').RefreshTimerManager;
var Utils = require('../utils/common.js');

/** @module thaliReplicationPeerAction */

/**
 * @classdesc Manages replicating information with a peer we have discovered
 * via notifications.
 *
 * @param {module:thaliNotificationClient.event:peerAdvertisesDataForUs} peerAdvertisesDataForUs
 * The notification that triggered this replication. This gives us the
 * information we need to create a connection as well the connection type
 * we need for the base class's constructor.
 * @param {PouchDB} PouchDB The PouchDB class constructor we are supposed to
 * use.
 * @param {string} dbName The name of the DB we will use both for local use as
 * well as remote use. Note that we will get the name for the remote database by
 * taking dbName and appending it to http://[hostAddress]:[portNumber]/[BASE_DB_PATH]/
 * [name] where hostAddress and portNumber are from the peerAdvertisesDataForUs
 * argument.
 * @param {Buffer} ourPublicKey The buffer containing our ECDH public key
 * @constructor
 */
function ThaliReplicationPeerAction(peerAdvertisesDataForUs,
                                    PouchDB,
                                    dbName,
                                    ourPublicKey) {
  assert(ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS * 1000 -
    ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS >
    1000, 'Need at least a seconds worth of clearance to make sure ' +
    'that at least one sync update will have gone out before we time out.');
  assert(peerAdvertisesDataForUs, 'there must be peerAdvertisesDataForUs');
  assert(PouchDB, 'there must be PouchDB');
  assert(dbName, 'there must be dbName');
  assert(ourPublicKey, 'there must be an ourPublicKey');

  ThaliReplicationPeerAction.super_.call(this, peerAdvertisesDataForUs.keyId,
    peerAdvertisesDataForUs.connectionType,
    ThaliReplicationPeerAction.ACTION_TYPE,
    peerAdvertisesDataForUs.pskIdentifyField,
    peerAdvertisesDataForUs.psk);

  this._peerAdvertisesDataForUs = peerAdvertisesDataForUs;
  this._PouchDB = PouchDB;
  this._dbName = dbName;
  this._ourPublicKey = ourPublicKey;
  this._localSeqManager = null;
  this._cancelReplication = null;
  this._resolve = null;
  this._reject = null;
  this._refreshTimerManager = null;
  this._completed = false;
}

util.inherits(ThaliReplicationPeerAction, ThaliPeerAction);

/**
 * The actionType we will use when calling the base class's constructor.
 *
 * @public
 * @readonly
 * @type {string}
 */
ThaliReplicationPeerAction.ACTION_TYPE = 'ReplicationAction';

/**
 * The number of seconds we will wait for an existing live replication to have
 * no changes before we terminate it.
 *
 * @public
 * @readonly
 * @type {number}
 */
ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS = 3;

/**
 * The number of milliseconds to wait between updating `_Local/<peer ID>` on the
 * remote machine. See
 * http://thaliproject.org/ReplicationAcrossDiscoveryProtocol/.
 *
 * @public
 * @readonly
 * @type {number}
 */
ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS = 200;

ThaliReplicationPeerAction.prototype.getPeerAdvertisesDataForUs = function () {
  return this._peerAdvertisesDataForUs;
};

/**
 * Creates a new replication action with the same constructor arguments as
 * this replication action.
 */
ThaliReplicationPeerAction.prototype.clone = function () {
  return new ThaliReplicationPeerAction(this._peerAdvertisesDataForUs,
    this._PouchDB, this._dbName, this._ourPublicKey);
};

/**
 * The replication timer is needed because by default we do live replications
 * which will keep a connection open to the remote server and send heartbeats
 * to keep things going. This means that our timers at lower levels in our
 * stack will see 'activity' and so won't time out a connection that isn't
 * actually doing useful work. This timer however is connected directly to the
 * changes feed and so can see if 'useful' work is happening and time out if it
 * is not.
 * @private
 */
ThaliReplicationPeerAction.prototype._replicationTimer = function () {
  var self = this;
  if (self._refreshTimerManager) {
    self._refreshTimerManager.stop();
  }
  self._refreshTimerManager = new RefreshTimerManager(
    ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS * 1000,
    function() {
      self._complete([new Error('No activity time out')]);
    });
  self._refreshTimerManager.start();
};

/**
 * When start is called we will start a replication with the remote peer using
 * the settings specified below. We will need to create the URL using the
 * hostAddress and portNumber from peerAdvertisesDataForUs. Also make sure to
 * set skip_setup to true.
 *
 * If we get an error that the database doesn't exist on the remote machine that
 * is fine, we're done. Although we should log a low priority error that we
 * tried to get to a database that doesn't exist. DO NOT log the peer ID.
 *
 * We then need to use db.replication.to with the remoteDB using the URL
 * specified in the constructor. This will be the local DB we will copy to. We
 * need to do things this way so we can set the AJAX options for PSK. We also
 * need to set both options.retry and options.live to true. See the changes
 * event below for some of the implications of this.
 *
 * We must hook these events from the replication object.
 *
 * paused - We need to log this with a very low priority log value just for
 * debugging purposes. But certainly nothing that would be recorded in
 * production.
 *
 * active - Log per the previous.
 *
 * denied - This is a genuine error, it should never happen so log with high
 * priority so we can investigate. Again, don't include any identifying
 * information, not even the DB name. It's a hint.
 *
 * complete - Return resolve(); if there was no error otherwise return Reject()
 * with an Error object with the string that either matches one of the {@link
 * module:thaliPeerAction~ThaliPeerAction.start} error strings or else something
 * appropriate. Even if there is an error we should always do a final write to
 * `_Local/<peer ID>` with the last_seq in the info object passed to complete.
 *
 * error - Log with reasonably high priority but with no identifying
 * information. Otherwise take no further action as the complete event should
 * also fire and we'll handle things there.
 *
 * __OPEN ISSUE:__ We actually need to investigate what kinds of err values come
 * back in order to determine if we can figure out if it was a connection error.
 * This is important for the thread pool to know. See the errors defined on
 * {@link module:thaliPeerAction~PeerAction.start}.
 *
 * change - If we don't see any changes on the replication for {@link
 * module:thaliReplicationPeerAction~ThaliReplicatonPeerAction.MAX_IDLE_PERIOD_SECONDS}
 * seconds then we will end the replication. The output from this event also
 * provides us with the current last_seq we have synch'd from the remote peer.
 * Per http://thaliproject.org/ReplicationAcrossDiscoveryProtocol/ we need to
 * update the remote `_Local/<peer ID>` document every
 * {@link module:thaliReplicationPeerAction~ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS}
 *
 * Make sure to keep the cancel object returned by the replicate call. Well
 * need it for kill.
 *
 * @param {http.Agent} httpAgentPool This is the HTTP connection pool to use
 * when creating HTTP requests related to this action. Note that this is where
 * the PSK related settings are specified.
 * @returns {Promise<?Error>}
 */
ThaliReplicationPeerAction.prototype.start = function (httpAgentPool) {
  var self = this;
  this._completed = false;

  return ThaliReplicationPeerAction.super_.prototype.start
    .call(this, httpAgentPool)
    .then(function () {
      var remoteUrl = 'https://' + self._peerAdvertisesDataForUs.hostAddress +
        ':' + self._peerAdvertisesDataForUs.portNumber +
        path.join(thaliConfig.BASE_DB_PATH, self._dbName);
      var ajaxOptions = {
        ajax : {
          agent: httpAgentPool
        },
        skip_setup: true// jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
      };

      var remoteDB = new self._PouchDB(remoteUrl, ajaxOptions);
      self._localSeqManager = new LocalSeqManager(
        ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS,
        remoteDB, self._ourPublicKey);
      self._replicationPromise = new Promise(function (resolve, reject) {
        self._resolve = resolve;
        self._reject = reject;
        self._replicationTimer();
        self._cancelReplication = remoteDB.replicate.to(self._dbName, {
          live: true
        })
        .on('paused', function (err) {
          logger.debug(
            'Got paused with - ',
            Utils.serializePouchError(err)
          );
        })
        .on('active', function () {
          logger.debug('Replication resumed');
        })
        .on('denied', function (err) {
          logger.warn(
            'We got denied on a PouchDB access, this really should ' +
            'not happen - ',
            Utils.serializePouchError(err)
          );
        })
        .on('complete', function (info) {
          self._complete(info.errors);
        })
        .on('error', function (err) {
          logger.debug(
            'Got error on replication - ',
            Utils.serializePouchError(err)
          );
          self._complete([err]);
        })
        .on('change', function (info) {
          self._replicationTimer();
          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
          self._localSeqManager
            .update(info.last_seq)
            .catch(function (err) {
              logger.debug(
                'Got error in update, waiting for main loop to ' +
                'detect and handle - ',
                Utils.serializePouchError(err)
              );
            });
          // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
        });
      });
      return self._replicationPromise;
    });
};

/**
 * Check the base class for the core functionality but in our case the key thing
 * is that we call the cancel object we got on the replication.
 *
 */
ThaliReplicationPeerAction.prototype.kill = function () {
  if (this.getActionState() === actionState.KILLED) {
    return;
  }
  ThaliReplicationPeerAction.super_.prototype.kill.call(this);

  if (this._refreshTimerManager) {
    this._refreshTimerManager.stop();
    this._refreshTimerManager = null;
  }
  if (this._cancelReplication) {
    this._cancelReplication.cancel();
  }
  if (this._localSeqManager) {
    this._localSeqManager.stop();
  }
};

function printErrorArray(errors) {
  var result = '';
  errors.forEach(function (error) {
    result += 'error: ' + error.message + ' ';
  });
  return result;
}

/**
 * @param {Array.<Error>} errors
 * @private
 */
ThaliReplicationPeerAction.prototype._complete =
  function (errors) {
    var self = this;
    if (this._completed) {
      logger.debug('We called _complete with ' + printErrorArray(errors) +
      ' but this is a second call and so we ignored it');
      return;
    }
    this._completed = true;
    logger.debug('We called _complete with ' + printErrorArray(errors) +
    ' and it caused us to complete');

    this.kill();

    assert(this._resolve, 'resolve should exist');
    assert(this._reject, 'reject should exist');
    var returnError = null;

    if (!errors || errors.length === 0) {
      this._resolve();
    } else {
      var isErrorResolved = errors.some(function (error) {
        switch (error.code) {
          case 'ECONNREFUSED': {
            returnError = new Error('Could not establish TCP connection');
            returnError.status = error.status;
            returnError.code = error.code;
            self._reject(returnError);
            return true;
          }
          case 'ECONNRESET': {
            returnError = new Error('Could establish TCP connection but ' +
              'couldn\'t keep it running');
            returnError.status = error.status;
            returnError.code = error.code;
            self._reject(returnError);
            return true;
          }
        }
        return false;
      });
      if (!isErrorResolved) {
        return this._reject(errors[0]);
      }
    }

    this._resolve = null;
    this._reject  = null;
  };

ThaliReplicationPeerAction.prototype.waitUntilKilled = function () {
  // We have just killed all requests.
  // We don't care if any request will end with an error.

  var promises = [];
  if (this._replicationPromise) {
    promises.push(
      this._replicationPromise.catch(function () {})
    );
  }
  if (this._localSeqManager) {
    promises.push(this._localSeqManager.waitUntilStopped());
  }
  return Promise.all(promises);
}

module.exports = ThaliReplicationPeerAction;
<commit_msg>we want to show and ignore errors if action is already killed<commit_after>'use strict';

var Promise = require('lie');
var util = require('util');
var path = require('path');
var ThaliPeerAction = require('../thaliPeerPool/thaliPeerAction');
var actionState = ThaliPeerAction.actionState;
var assert = require('assert');
var thaliConfig = require('../thaliConfig');
var logger = require('../../ThaliLogger')('thaliReplicationPeerAction');
var LocalSeqManager = require('./localSeqManager');
var RefreshTimerManager = require('./utilities').RefreshTimerManager;
var Utils = require('../utils/common.js');

/** @module thaliReplicationPeerAction */

/**
 * @classdesc Manages replicating information with a peer we have discovered
 * via notifications.
 *
 * @param {module:thaliNotificationClient.event:peerAdvertisesDataForUs} peerAdvertisesDataForUs
 * The notification that triggered this replication. This gives us the
 * information we need to create a connection as well the connection type
 * we need for the base class's constructor.
 * @param {PouchDB} PouchDB The PouchDB class constructor we are supposed to
 * use.
 * @param {string} dbName The name of the DB we will use both for local use as
 * well as remote use. Note that we will get the name for the remote database by
 * taking dbName and appending it to http://[hostAddress]:[portNumber]/[BASE_DB_PATH]/
 * [name] where hostAddress and portNumber are from the peerAdvertisesDataForUs
 * argument.
 * @param {Buffer} ourPublicKey The buffer containing our ECDH public key
 * @constructor
 */
function ThaliReplicationPeerAction(peerAdvertisesDataForUs,
                                    PouchDB,
                                    dbName,
                                    ourPublicKey) {
  assert(ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS * 1000 -
    ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS >
    1000, 'Need at least a seconds worth of clearance to make sure ' +
    'that at least one sync update will have gone out before we time out.');
  assert(peerAdvertisesDataForUs, 'there must be peerAdvertisesDataForUs');
  assert(PouchDB, 'there must be PouchDB');
  assert(dbName, 'there must be dbName');
  assert(ourPublicKey, 'there must be an ourPublicKey');

  ThaliReplicationPeerAction.super_.call(this, peerAdvertisesDataForUs.keyId,
    peerAdvertisesDataForUs.connectionType,
    ThaliReplicationPeerAction.ACTION_TYPE,
    peerAdvertisesDataForUs.pskIdentifyField,
    peerAdvertisesDataForUs.psk);

  this._peerAdvertisesDataForUs = peerAdvertisesDataForUs;
  this._PouchDB = PouchDB;
  this._dbName = dbName;
  this._ourPublicKey = ourPublicKey;
  this._localSeqManager = null;
  this._cancelReplication = null;
  this._resolve = null;
  this._reject = null;
  this._refreshTimerManager = null;
  this._completed = false;
}

util.inherits(ThaliReplicationPeerAction, ThaliPeerAction);

/**
 * The actionType we will use when calling the base class's constructor.
 *
 * @public
 * @readonly
 * @type {string}
 */
ThaliReplicationPeerAction.ACTION_TYPE = 'ReplicationAction';

/**
 * The number of seconds we will wait for an existing live replication to have
 * no changes before we terminate it.
 *
 * @public
 * @readonly
 * @type {number}
 */
ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS = 3;

/**
 * The number of milliseconds to wait between updating `_Local/<peer ID>` on the
 * remote machine. See
 * http://thaliproject.org/ReplicationAcrossDiscoveryProtocol/.
 *
 * @public
 * @readonly
 * @type {number}
 */
ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS = 200;

ThaliReplicationPeerAction.prototype.getPeerAdvertisesDataForUs = function () {
  return this._peerAdvertisesDataForUs;
};

/**
 * Creates a new replication action with the same constructor arguments as
 * this replication action.
 */
ThaliReplicationPeerAction.prototype.clone = function () {
  return new ThaliReplicationPeerAction(this._peerAdvertisesDataForUs,
    this._PouchDB, this._dbName, this._ourPublicKey);
};

/**
 * The replication timer is needed because by default we do live replications
 * which will keep a connection open to the remote server and send heartbeats
 * to keep things going. This means that our timers at lower levels in our
 * stack will see 'activity' and so won't time out a connection that isn't
 * actually doing useful work. This timer however is connected directly to the
 * changes feed and so can see if 'useful' work is happening and time out if it
 * is not.
 * @private
 */
ThaliReplicationPeerAction.prototype._replicationTimer = function () {
  var self = this;
  if (self._refreshTimerManager) {
    self._refreshTimerManager.stop();
  }
  self._refreshTimerManager = new RefreshTimerManager(
    ThaliReplicationPeerAction.MAX_IDLE_PERIOD_SECONDS * 1000,
    function() {
      self._complete([new Error('No activity time out')]);
    });
  self._refreshTimerManager.start();
};

/**
 * When start is called we will start a replication with the remote peer using
 * the settings specified below. We will need to create the URL using the
 * hostAddress and portNumber from peerAdvertisesDataForUs. Also make sure to
 * set skip_setup to true.
 *
 * If we get an error that the database doesn't exist on the remote machine that
 * is fine, we're done. Although we should log a low priority error that we
 * tried to get to a database that doesn't exist. DO NOT log the peer ID.
 *
 * We then need to use db.replication.to with the remoteDB using the URL
 * specified in the constructor. This will be the local DB we will copy to. We
 * need to do things this way so we can set the AJAX options for PSK. We also
 * need to set both options.retry and options.live to true. See the changes
 * event below for some of the implications of this.
 *
 * We must hook these events from the replication object.
 *
 * paused - We need to log this with a very low priority log value just for
 * debugging purposes. But certainly nothing that would be recorded in
 * production.
 *
 * active - Log per the previous.
 *
 * denied - This is a genuine error, it should never happen so log with high
 * priority so we can investigate. Again, don't include any identifying
 * information, not even the DB name. It's a hint.
 *
 * complete - Return resolve(); if there was no error otherwise return Reject()
 * with an Error object with the string that either matches one of the {@link
 * module:thaliPeerAction~ThaliPeerAction.start} error strings or else something
 * appropriate. Even if there is an error we should always do a final write to
 * `_Local/<peer ID>` with the last_seq in the info object passed to complete.
 *
 * error - Log with reasonably high priority but with no identifying
 * information. Otherwise take no further action as the complete event should
 * also fire and we'll handle things there.
 *
 * __OPEN ISSUE:__ We actually need to investigate what kinds of err values come
 * back in order to determine if we can figure out if it was a connection error.
 * This is important for the thread pool to know. See the errors defined on
 * {@link module:thaliPeerAction~PeerAction.start}.
 *
 * change - If we don't see any changes on the replication for {@link
 * module:thaliReplicationPeerAction~ThaliReplicatonPeerAction.MAX_IDLE_PERIOD_SECONDS}
 * seconds then we will end the replication. The output from this event also
 * provides us with the current last_seq we have synch'd from the remote peer.
 * Per http://thaliproject.org/ReplicationAcrossDiscoveryProtocol/ we need to
 * update the remote `_Local/<peer ID>` document every
 * {@link module:thaliReplicationPeerAction~ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS}
 *
 * Make sure to keep the cancel object returned by the replicate call. Well
 * need it for kill.
 *
 * @param {http.Agent} httpAgentPool This is the HTTP connection pool to use
 * when creating HTTP requests related to this action. Note that this is where
 * the PSK related settings are specified.
 * @returns {Promise<?Error>}
 */
ThaliReplicationPeerAction.prototype.start = function (httpAgentPool) {
  var self = this;
  this._completed = false;

  return ThaliReplicationPeerAction.super_.prototype.start
    .call(this, httpAgentPool)
    .then(function () {
      var remoteUrl = 'https://' + self._peerAdvertisesDataForUs.hostAddress +
        ':' + self._peerAdvertisesDataForUs.portNumber +
        path.join(thaliConfig.BASE_DB_PATH, self._dbName);
      var ajaxOptions = {
        ajax : {
          agent: httpAgentPool
        },
        skip_setup: true// jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
      };

      var remoteDB = new self._PouchDB(remoteUrl, ajaxOptions);
      self._localSeqManager = new LocalSeqManager(
        ThaliReplicationPeerAction.PUSH_LAST_SYNC_UPDATE_MILLISECONDS,
        remoteDB, self._ourPublicKey);
      self._replicationPromise = new Promise(function (resolve, reject) {
        self._resolve = resolve;
        self._reject = reject;
        self._replicationTimer();
        self._cancelReplication = remoteDB.replicate.to(self._dbName, {
          live: true
        })
        .on('paused', function (err) {
          logger.debug(
            'Got paused with - ',
            Utils.serializePouchError(err)
          );
        })
        .on('active', function () {
          logger.debug('Replication resumed');
        })
        .on('denied', function (err) {
          logger.warn(
            'We got denied on a PouchDB access, this really should ' +
            'not happen - ',
            Utils.serializePouchError(err)
          );
        })
        .on('complete', function (info) {
          self._complete(info.errors);
        })
        .on('error', function (err) {
          logger.debug(
            'Got error on replication - ',
            Utils.serializePouchError(err)
          );
          self._complete([err]);
        })
        .on('change', function (info) {
          self._replicationTimer();
          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
          self._localSeqManager
            .update(info.last_seq)
            .catch(function (err) {
              if (self.getActionState() === actionState.KILLED) {
                logger.debug(
                  'Got error in update: %s, but we are already killed and so we ignored it',
                  Utils.serializePouchError(err)
                );
              } else {
                logger.debug(
                  'Got error in update: %s, waiting for main loop to detect and handle',
                  Utils.serializePouchError(err)
                );
              }
            });
          // jscs:enable requireCamelCaseOrUpperCaseIdentifiers
        });
      });
      return self._replicationPromise;
    });
};

/**
 * Check the base class for the core functionality but in our case the key thing
 * is that we call the cancel object we got on the replication.
 *
 */
ThaliReplicationPeerAction.prototype.kill = function () {
  if (this.getActionState() === actionState.KILLED) {
    return;
  }
  ThaliReplicationPeerAction.super_.prototype.kill.call(this);

  if (this._refreshTimerManager) {
    this._refreshTimerManager.stop();
    this._refreshTimerManager = null;
  }
  if (this._cancelReplication) {
    this._cancelReplication.cancel();
  }
  if (this._localSeqManager) {
    this._localSeqManager.stop();
  }
};

function printErrorArray(errors) {
  var result = '';
  errors.forEach(function (error) {
    result += 'error: ' + error.message + ' ';
  });
  return result;
}

/**
 * @param {Array.<Error>} errors
 * @private
 */
ThaliReplicationPeerAction.prototype._complete =
  function (errors) {
    var self = this;
    if (this._completed) {
      logger.debug('We called _complete with ' + printErrorArray(errors) +
      ' but this is a second call and so we ignored it');
      return;
    }
    this._completed = true;

    assert(this._resolve, 'resolve should exist');
    assert(this._reject, 'reject should exist');

    if (this.getActionState() === actionState.KILLED) {
      logger.debug('We called _complete with ' + printErrorArray(errors) +
      ' but we are already killed and so we ignored it');
      this._resolve();
      return;
    }

    logger.debug('We called _complete with ' + printErrorArray(errors) +
    ' and it caused us to complete');

    this.kill();

    var returnError = null;

    if (!errors || errors.length === 0) {
      this._resolve();
    } else {
      var isErrorResolved = errors.some(function (error) {
        switch (error.code) {
          case 'ECONNREFUSED': {
            returnError = new Error('Could not establish TCP connection');
            returnError.status = error.status;
            returnError.code = error.code;
            self._reject(returnError);
            return true;
          }
          case 'ECONNRESET': {
            returnError = new Error('Could establish TCP connection but ' +
              'couldn\'t keep it running');
            returnError.status = error.status;
            returnError.code = error.code;
            self._reject(returnError);
            return true;
          }
        }
        return false;
      });
      if (!isErrorResolved) {
        return this._reject(errors[0]);
      }
    }

    this._resolve = null;
    this._reject  = null;
  };

ThaliReplicationPeerAction.prototype.waitUntilKilled = function () {
  // We have just killed all requests.
  // We don't care if any request will end with an error.

  var promises = [];
  if (this._replicationPromise) {
    promises.push(
      this._replicationPromise.catch(function () {})
    );
  }
  if (this._localSeqManager) {
    promises.push(this._localSeqManager.waitUntilStopped());
  }
  return Promise.all(promises);
}

module.exports = ThaliReplicationPeerAction;
<|endoftext|>