<commit_before>/*    Copyright 2016-2020 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict'

const log = require('../net2/logger.js')(__filename);

const util = require('util');
const minimatch = require("minimatch");
const cronParser = require('cron-parser');
const HostTool = require('../net2/HostTool.js')
const hostTool = new HostTool()
const Constants = require('../net2/Constants.js');

const _ = require('lodash');
const flat = require('flat');
const iptool = require('ip');
const POLICY_MIN_EXPIRE_TIME = 60 // if policy is going to expire in 60 seconds, don't bother to enforce it.

function arraysEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (!a && !_.isNil(a) || !b && !_.isNil(b)) return false // exclude false, NaN
  if (_.isEmpty(a) && _.isEmpty(b)) return true;  // [], undefined
  if (!Array.isArray(a) || !Array.isArray(b)) return false

  return _.isEqual(a.sort(), b.sort())
}

class Policy {
  constructor(raw) {
    if (!raw) throw new Error("Empty policy payload");
    if (!raw.type && !raw['i.type']) throw new Error("Invalid policy payload");
    if (raw.type == 'internet') throw new Error(`Invalid policy type ${raw.type}`);

    Object.assign(this, raw);

    this.parseRedisfyArray(raw);

    if (this.scope) {
      // convert vpn profiles in "scope" field to "vpnProfile" field
      const vpnProfiles = this.scope.filter(v => v.startsWith(`${Constants.NS_VPN_PROFILE}:`)).map(v => v.substring(`${Constants.NS_VPN_PROFILE}:`.length));
      this.scope = this.scope.filter(v => !v.startsWith(`${Constants.NS_VPN_PROFILE}:`));
      this.vpnProfile = (this.vpnProfile || []).concat(vpnProfiles).filter((v, i, a) => a.indexOf(v) === i);
      if (!_.isArray(this.scope) || _.isEmpty(this.scope))
        delete this.scope;
      if (!_.isArray(this.vpnProfile) || _.isEmpty(this.vpnProfile))
        delete this.vpnProfile;
    }

    this.upnp = false;
    if (raw.upnp)
      this.upnp = JSON.parse(raw.upnp);

    if (raw.priority)
      this.priority = Number(raw.priority);

    if (raw.transferredBytes)
      this.transferredBytes = Number(raw.transferredBytes);

    if (raw.transferredPackets)
      this.transferredPackets = Number(raw.transferredPackets);

    if (raw.avgPacketBytes)
      this.avgPacketBytes = Number(raw.avgPacketBytes);

    if (!_.isEmpty(raw.ipttl))
      this.ipttl = Number(raw.ipttl);

    this.dnsmasq_only = false;
    if (raw.dnsmasq_only)
      this.dnsmasq_only = JSON.parse(raw.dnsmasq_only);

    if (!raw.direction)
      this.direction = "bidirection";

    if (!raw.action)
      this.action = "block";

    if (raw.expire === "") {
      delete this.expire;
    } else if (raw.expire && _.isString(raw.expire)) {
      try {
        this.expire = parseInt(raw.expire)
      } catch (e) {
        log.error("Failed to parse policy expire time:", raw.expire, e);
        delete this.expire;
      }
    }

    if (raw.cronTime === "") {
      delete this.cronTime;
    }

    // backward compatibilities
    if (this['i.type']) {
      this.type = this['i.type'];
      delete this['i.type'];
    }
    if (this['i.target']) {
      this.target = this['i.target'];
      delete this['i.target'];
    }

    if (this.target && this.type) {
      switch (this.type) {
        case "mac":
          this.target = this.target.toUpperCase(); // always upper case for mac address
          break;
        case "dns":
        case "domain":
          this.target = this.target.toLowerCase(); // always lower case for domain block
          break;
        default:
        // do nothing;
      }
    }

    this.timestamp = this.timestamp || new Date() / 1000;
  }

  isEqualToPolicy(policy) {
    if (!policy) {
      return false
    }
    if (!(policy instanceof Policy))
      policy = new Policy(policy) // leverage the constructor for compatibilities conversion

    if (
      this.type === policy.type &&
      this.target === policy.target &&
      this.expire === policy.expire &&
      this.cronTime === policy.cronTime &&
      this.remotePort === policy.remotePort &&
      this.localPort === policy.localPort &&
      this.protocol === policy.protocol &&
      this.direction === policy.direction &&
      this.action === policy.action &&
      this.upnp === policy.upnp &&
      this.dnsmasq_only === policy.dnsmasq_only &&
      this.trafficDirection === policy.trafficDirection &&
      this.transferredBytes === policy.transferredBytes &&
      this.transferredPackets === policy.transferredPackets &&
      this.avgPacketBytes === policy.avgPacketBytes &&
      this.parentRgId === policy.parentRgId &&
      this.targetRgId === policy.targetRgId &&
      this.ipttl === policy.ipttl &&
      // ignore scope if type is mac
      (this.type == 'mac' && hostTool.isMacAddress(this.target) || arraysEqual(this.scope, policy.scope)) &&
      arraysEqual(this.tag, policy.tag) &&
      arraysEqual(this.vpnProfile, policy.vpnProfile)
    ) {
      return true
    }

    return false
  }
  getIdleInfo() {
    if (this.idleTs) {
      const idleTs = Number(this.idleTs);
      const now = new Date() / 1000;
      const idleTsFromNow = idleTs - now;
      const idleExpireSoon = idleTs < (now + POLICY_MIN_EXPIRE_TIME);
      return {
        idleTsFromNow, idleExpireSoon
      }
    } else {
      return null;
    }
  }

  isExpired() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire) < new Date() / 1000
  }

  willExpireSoon() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire) < new Date() / 1000 + POLICY_MIN_EXPIRE_TIME
  }

  getWhenExpired() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire)
  }

  getExpireDiffFromNow() {
    return this.getWhenExpired() - new Date() / 1000
  }

  isDisabled() {
    return this.disabled && this.disabled == '1'
  }
  inSchedule(alarmTimestamp) {
    const sysManager = require('../net2/SysManager.js');
    const cronTime = this.cronTime;
    const duration = parseFloat(this.duration); // in seconds
    const interval = cronParser.parseExpression(cronTime, { tz: sysManager.getTimezone() });
    const lastDate = interval.prev().getTime() / 1000;
    log.info(`lastDate: ${lastDate}, duration: ${duration}, alarmTimestamp:${alarmTimestamp}`);

    if (alarmTimestamp > lastDate && alarmTimestamp < lastDate + duration) {
      return true
    } else {
      return false
    }
  }

  match(alarm) {

    if (!alarm.needPolicyMatch()) {
      return false;
    }

    if (this.action == 'allow') {
      return false;
    }

    if (this.isExpired()) {
      return false // always return unmatched if policy is already expired
    }
    if (this.cronTime && this.duration && !this.inSchedule(alarm.alarmTimestamp)) {
      return false;
    }

    if (
      this.scope &&
      _.isArray(this.scope) &&
      !_.isEmpty(this.scope) &&
      alarm['p.device.mac'] &&
      !this.scope.includes(alarm['p.device.mac'])
    ) {
      return false; // scope not match
    }

    if (
      this.vpnProfile &&
      _.isArray(this.vpnProfile) &&
      !_.isEmpty(this.vpnProfile) &&
      alarm['p.device.vpnProfile'] &&
      !this.vpnProfile.includes(alarm['p.device.vpnProfile'])
    ) {
      return false; // vpn profile not match
    }

    if (
      this.tag &&
      _.isArray(this.tag) &&
      !_.isEmpty(this.tag) &&
      alarm['p.intf.id'] &&
      !this.tag.includes(Policy.INTF_PREFIX + alarm['p.intf.id'])
    ) {
      return false; // tag not match
    }

    if (
      this.tag &&
      _.isArray(this.tag) &&
      !_.isEmpty(this.tag) &&
      _.has(alarm, 'p.tag.ids') &&
      !_.isEmpty(alarm['p.tag.ids'])
    ) {
      let found = false;
      for (let index = 0; index < alarm['p.tag.ids'].length; index++) {
        const tag = alarm['p.tag.ids'][index];
        if (this.tag.includes(Policy.TAG_PREFIX + tag)) {
          found = true;
        }
      }

      if (!found) {
        return false;
      }
    }

    if (this.localPort && alarm['p.device.port']) {
      const notInRange = this.portInRange(this.localPort, alarm['p.device.port']);
      if (!notInRange) return false;
    }

    if (this.remotePort && alarm['p.dest.port']) {
      const notInRange = this.portInRange(this.remotePort, alarm['p.dest.port']);
      if (!notInRange) return false;
    }

    // for each policy type
    switch (this.type) {
      case "ip":
        if (alarm['p.dest.ip']) {
          return this.target === alarm['p.dest.ip']
        } else {
          return false
        }
        break
      case "net":
        if (alarm['p.dest.ip']) {
          return iptool.cidrSubnet(this.target).contains(alarm['p.dest.ip'])
        } else {
          return false
        }
        break

      case "dns":
      case "domain":
        if (alarm['p.dest.name']) {
          return minimatch(alarm['p.dest.name'], `*.${this.target}`) ||
            alarm['p.dest.name'] === this.target
        } else {
          return false
        }
        break

      case "mac":
        if (alarm['p.device.mac']) {
          return alarm['p.device.mac'] === this.target
        } else {
          return false
        }
        break

      case "category":
        if (alarm['p.dest.category']) {
          return alarm['p.dest.category'] === this.target;
        } else {
          return false;
        }
        break

      case "devicePort":
        if (!alarm['p.device.mac']) return false;

        if (alarm["p.device.port"] &&
          alarm["p.protocol"]
        ) {
          let alarmTarget = util.format("%s:%s:%s",
            alarm["p.device.mac"],
            alarm["p.device.port"],
            alarm["p.protocol"]
          )
          return alarmTarget === this.target;
        }

        if (alarm["p.upnp.private.port"] &&
          alarm["p.upnp.protocol"]
        ) {
          let alarmTarget = util.format("%s:%s:%s",
            alarm["p.device.mac"],
            alarm["p.upnp.private.port"],
            alarm["p.upnp.protocol"]
          )
          return alarmTarget === this.target;
        }

        return false;
        break
      case "remotePort":
        if (alarm['p.dest.port']) {
          return this.portInRange(this.target, alarm['p.dest.port'])
        } else {
          return false;
        }
        break;
      case 'country':
        if (alarm['p.dest.country']) {
          return alarm['p.dest.country'] == this.target;
        } else {
          return false;
        }
        break;
      default:
        return false
        break
    }
  }

  redisfyArray(p) {
    for (const key of Policy.ARRAR_VALUE_KEYS) {
      if (p[key]) {
        if (p[key].length > 0)
          p[key] = JSON.stringify(p[key]);
        else
          delete p[key];
      }
    }
  }

  parseRedisfyArray(raw) {
    for (const key of Policy.ARRAR_VALUE_KEYS) {
      if (raw[key]) {
        if (_.isString(raw[key])) {
          try {
            this[key] = JSON.parse(raw[key])
          } catch (e) {
            log.error(`Failed to parse policy ${key} string:`, raw[key], e)
          }
        } else if (_.isArray(raw[key])) {
          this[key] = Array.from(raw[key]); // clone array to avoide side effects
        } else {
          log.error(`Unsupported ${key}`, raw[key])
        }

        if (!_.isArray(this[key]) || _.isEmpty(this[key]))
          delete this[key];
      }
    }
  }

  // return a new object ready for redis writing
  redisfy() {
    let p = JSON.parse(JSON.stringify(this))

    // convert array to string so that redis can store it as value
    this.redisfyArray(p);

    if (p.expire === "") {
      delete p.expire;
    }

    if (p.cronTime === "") {
      delete p.cronTime;
    }

    return flat.flatten(p);
  }

  portInRange(portRange, port) {
    // portRange 555 || 555-666
    // port '600' || '[52492,61734]'
    portRange = (portRange || '').split('-');
    if (portRange.length == 1) portRange.push(portRange[0]); // [555,555]
    if (_.isString(port)) {
      try {
        port = JSON.parse(port);
      } catch (e) {
        port = (port || 0) * 1;
      }
    }
    if (_.isArray(port)) {
      let allInRange = true;
      for (const p of port) {
        allInRange = allInRange && portRange[0] * 1 <= p && p <= portRange[1] * 1;
        if (!allInRange) return false;
      }
    } else {
      return portRange[0] * 1 <= port && port <= portRange[1] * 1;
    }
  }
}

Policy.ARRAR_VALUE_KEYS = ["scope", "tag", "vpnProfile", "applyRules"];
Policy.INTF_PREFIX = "intf:";
Policy.TAG_PREFIX = "tag:";

module.exports = Policy
<commit_msg>add wanUUID into isEqualToPolicy in Policy.js<commit_after>/*    Copyright 2016-2020 Firewalla Inc.
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict'

const log = require('../net2/logger.js')(__filename);

const util = require('util');
const minimatch = require("minimatch");
const cronParser = require('cron-parser');
const HostTool = require('../net2/HostTool.js')
const hostTool = new HostTool()
const Constants = require('../net2/Constants.js');

const _ = require('lodash');
const flat = require('flat');
const iptool = require('ip');
const POLICY_MIN_EXPIRE_TIME = 60 // if policy is going to expire in 60 seconds, don't bother to enforce it.

function arraysEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (!a && !_.isNil(a) || !b && !_.isNil(b)) return false // exclude false, NaN
  if (_.isEmpty(a) && _.isEmpty(b)) return true;  // [], undefined
  if (!Array.isArray(a) || !Array.isArray(b)) return false

  return _.isEqual(a.sort(), b.sort())
}

class Policy {
  constructor(raw) {
    if (!raw) throw new Error("Empty policy payload");
    if (!raw.type && !raw['i.type']) throw new Error("Invalid policy payload");
    if (raw.type == 'internet') throw new Error(`Invalid policy type ${raw.type}`);

    Object.assign(this, raw);

    this.parseRedisfyArray(raw);

    if (this.scope) {
      // convert vpn profiles in "scope" field to "vpnProfile" field
      const vpnProfiles = this.scope.filter(v => v.startsWith(`${Constants.NS_VPN_PROFILE}:`)).map(v => v.substring(`${Constants.NS_VPN_PROFILE}:`.length));
      this.scope = this.scope.filter(v => !v.startsWith(`${Constants.NS_VPN_PROFILE}:`));
      this.vpnProfile = (this.vpnProfile || []).concat(vpnProfiles).filter((v, i, a) => a.indexOf(v) === i);
      if (!_.isArray(this.scope) || _.isEmpty(this.scope))
        delete this.scope;
      if (!_.isArray(this.vpnProfile) || _.isEmpty(this.vpnProfile))
        delete this.vpnProfile;
    }

    this.upnp = false;
    if (raw.upnp)
      this.upnp = JSON.parse(raw.upnp);

    if (raw.priority)
      this.priority = Number(raw.priority);

    if (raw.transferredBytes)
      this.transferredBytes = Number(raw.transferredBytes);

    if (raw.transferredPackets)
      this.transferredPackets = Number(raw.transferredPackets);

    if (raw.avgPacketBytes)
      this.avgPacketBytes = Number(raw.avgPacketBytes);

    if (!_.isEmpty(raw.ipttl))
      this.ipttl = Number(raw.ipttl);

    this.dnsmasq_only = false;
    if (raw.dnsmasq_only)
      this.dnsmasq_only = JSON.parse(raw.dnsmasq_only);

    if (!raw.direction)
      this.direction = "bidirection";

    if (!raw.action)
      this.action = "block";

    if (raw.expire === "") {
      delete this.expire;
    } else if (raw.expire && _.isString(raw.expire)) {
      try {
        this.expire = parseInt(raw.expire)
      } catch (e) {
        log.error("Failed to parse policy expire time:", raw.expire, e);
        delete this.expire;
      }
    }

    if (raw.cronTime === "") {
      delete this.cronTime;
    }

    // backward compatibilities
    if (this['i.type']) {
      this.type = this['i.type'];
      delete this['i.type'];
    }
    if (this['i.target']) {
      this.target = this['i.target'];
      delete this['i.target'];
    }

    if (this.target && this.type) {
      switch (this.type) {
        case "mac":
          this.target = this.target.toUpperCase(); // always upper case for mac address
          break;
        case "dns":
        case "domain":
          this.target = this.target.toLowerCase(); // always lower case for domain block
          break;
        default:
        // do nothing;
      }
    }

    this.timestamp = this.timestamp || new Date() / 1000;
  }

  isEqualToPolicy(policy) {
    if (!policy) {
      return false
    }
    if (!(policy instanceof Policy))
      policy = new Policy(policy) // leverage the constructor for compatibilities conversion

    if (
      this.type === policy.type &&
      this.target === policy.target &&
      this.expire === policy.expire &&
      this.cronTime === policy.cronTime &&
      this.remotePort === policy.remotePort &&
      this.localPort === policy.localPort &&
      this.protocol === policy.protocol &&
      this.direction === policy.direction &&
      this.action === policy.action &&
      this.upnp === policy.upnp &&
      this.dnsmasq_only === policy.dnsmasq_only &&
      this.trafficDirection === policy.trafficDirection &&
      this.transferredBytes === policy.transferredBytes &&
      this.transferredPackets === policy.transferredPackets &&
      this.avgPacketBytes === policy.avgPacketBytes &&
      this.parentRgId === policy.parentRgId &&
      this.targetRgId === policy.targetRgId &&
      this.ipttl === policy.ipttl &&
      this.wanUUID === policy.wanUUID &&
      // ignore scope if type is mac
      (this.type == 'mac' && hostTool.isMacAddress(this.target) || arraysEqual(this.scope, policy.scope)) &&
      arraysEqual(this.tag, policy.tag) &&
      arraysEqual(this.vpnProfile, policy.vpnProfile)
    ) {
      return true
    }

    return false
  }
  getIdleInfo() {
    if (this.idleTs) {
      const idleTs = Number(this.idleTs);
      const now = new Date() / 1000;
      const idleTsFromNow = idleTs - now;
      const idleExpireSoon = idleTs < (now + POLICY_MIN_EXPIRE_TIME);
      return {
        idleTsFromNow, idleExpireSoon
      }
    } else {
      return null;
    }
  }

  isExpired() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire) < new Date() / 1000
  }

  willExpireSoon() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire) < new Date() / 1000 + POLICY_MIN_EXPIRE_TIME
  }

  getWhenExpired() {
    const expire = this.expire || NaN
    const activatedTime = this.activatedTime || this.timestamp
    return parseFloat(activatedTime) + parseFloat(expire)
  }

  getExpireDiffFromNow() {
    return this.getWhenExpired() - new Date() / 1000
  }

  isDisabled() {
    return this.disabled && this.disabled == '1'
  }
  inSchedule(alarmTimestamp) {
    const sysManager = require('../net2/SysManager.js');
    const cronTime = this.cronTime;
    const duration = parseFloat(this.duration); // in seconds
    const interval = cronParser.parseExpression(cronTime, { tz: sysManager.getTimezone() });
    const lastDate = interval.prev().getTime() / 1000;
    log.info(`lastDate: ${lastDate}, duration: ${duration}, alarmTimestamp:${alarmTimestamp}`);

    if (alarmTimestamp > lastDate && alarmTimestamp < lastDate + duration) {
      return true
    } else {
      return false
    }
  }

  match(alarm) {

    if (!alarm.needPolicyMatch()) {
      return false;
    }

    if (this.action == 'allow') {
      return false;
    }

    if (this.isExpired()) {
      return false // always return unmatched if policy is already expired
    }
    if (this.cronTime && this.duration && !this.inSchedule(alarm.alarmTimestamp)) {
      return false;
    }

    if (
      this.scope &&
      _.isArray(this.scope) &&
      !_.isEmpty(this.scope) &&
      alarm['p.device.mac'] &&
      !this.scope.includes(alarm['p.device.mac'])
    ) {
      return false; // scope not match
    }

    if (
      this.vpnProfile &&
      _.isArray(this.vpnProfile) &&
      !_.isEmpty(this.vpnProfile) &&
      alarm['p.device.vpnProfile'] &&
      !this.vpnProfile.includes(alarm['p.device.vpnProfile'])
    ) {
      return false; // vpn profile not match
    }

    if (
      this.tag &&
      _.isArray(this.tag) &&
      !_.isEmpty(this.tag) &&
      alarm['p.intf.id'] &&
      !this.tag.includes(Policy.INTF_PREFIX + alarm['p.intf.id'])
    ) {
      return false; // tag not match
    }

    if (
      this.tag &&
      _.isArray(this.tag) &&
      !_.isEmpty(this.tag) &&
      _.has(alarm, 'p.tag.ids') &&
      !_.isEmpty(alarm['p.tag.ids'])
    ) {
      let found = false;
      for (let index = 0; index < alarm['p.tag.ids'].length; index++) {
        const tag = alarm['p.tag.ids'][index];
        if (this.tag.includes(Policy.TAG_PREFIX + tag)) {
          found = true;
        }
      }

      if (!found) {
        return false;
      }
    }

    if (this.localPort && alarm['p.device.port']) {
      const notInRange = this.portInRange(this.localPort, alarm['p.device.port']);
      if (!notInRange) return false;
    }

    if (this.remotePort && alarm['p.dest.port']) {
      const notInRange = this.portInRange(this.remotePort, alarm['p.dest.port']);
      if (!notInRange) return false;
    }

    // for each policy type
    switch (this.type) {
      case "ip":
        if (alarm['p.dest.ip']) {
          return this.target === alarm['p.dest.ip']
        } else {
          return false
        }
        break
      case "net":
        if (alarm['p.dest.ip']) {
          return iptool.cidrSubnet(this.target).contains(alarm['p.dest.ip'])
        } else {
          return false
        }
        break

      case "dns":
      case "domain":
        if (alarm['p.dest.name']) {
          return minimatch(alarm['p.dest.name'], `*.${this.target}`) ||
            alarm['p.dest.name'] === this.target
        } else {
          return false
        }
        break

      case "mac":
        if (alarm['p.device.mac']) {
          return alarm['p.device.mac'] === this.target
        } else {
          return false
        }
        break

      case "category":
        if (alarm['p.dest.category']) {
          return alarm['p.dest.category'] === this.target;
        } else {
          return false;
        }
        break

      case "devicePort":
        if (!alarm['p.device.mac']) return false;

        if (alarm["p.device.port"] &&
          alarm["p.protocol"]
        ) {
          let alarmTarget = util.format("%s:%s:%s",
            alarm["p.device.mac"],
            alarm["p.device.port"],
            alarm["p.protocol"]
          )
          return alarmTarget === this.target;
        }

        if (alarm["p.upnp.private.port"] &&
          alarm["p.upnp.protocol"]
        ) {
          let alarmTarget = util.format("%s:%s:%s",
            alarm["p.device.mac"],
            alarm["p.upnp.private.port"],
            alarm["p.upnp.protocol"]
          )
          return alarmTarget === this.target;
        }

        return false;
        break
      case "remotePort":
        if (alarm['p.dest.port']) {
          return this.portInRange(this.target, alarm['p.dest.port'])
        } else {
          return false;
        }
        break;
      case 'country':
        if (alarm['p.dest.country']) {
          return alarm['p.dest.country'] == this.target;
        } else {
          return false;
        }
        break;
      default:
        return false
        break
    }
  }

  redisfyArray(p) {
    for (const key of Policy.ARRAR_VALUE_KEYS) {
      if (p[key]) {
        if (p[key].length > 0)
          p[key] = JSON.stringify(p[key]);
        else
          delete p[key];
      }
    }
  }

  parseRedisfyArray(raw) {
    for (const key of Policy.ARRAR_VALUE_KEYS) {
      if (raw[key]) {
        if (_.isString(raw[key])) {
          try {
            this[key] = JSON.parse(raw[key])
          } catch (e) {
            log.error(`Failed to parse policy ${key} string:`, raw[key], e)
          }
        } else if (_.isArray(raw[key])) {
          this[key] = Array.from(raw[key]); // clone array to avoide side effects
        } else {
          log.error(`Unsupported ${key}`, raw[key])
        }

        if (!_.isArray(this[key]) || _.isEmpty(this[key]))
          delete this[key];
      }
    }
  }

  // return a new object ready for redis writing
  redisfy() {
    let p = JSON.parse(JSON.stringify(this))

    // convert array to string so that redis can store it as value
    this.redisfyArray(p);

    if (p.expire === "") {
      delete p.expire;
    }

    if (p.cronTime === "") {
      delete p.cronTime;
    }

    return flat.flatten(p);
  }

  portInRange(portRange, port) {
    // portRange 555 || 555-666
    // port '600' || '[52492,61734]'
    portRange = (portRange || '').split('-');
    if (portRange.length == 1) portRange.push(portRange[0]); // [555,555]
    if (_.isString(port)) {
      try {
        port = JSON.parse(port);
      } catch (e) {
        port = (port || 0) * 1;
      }
    }
    if (_.isArray(port)) {
      let allInRange = true;
      for (const p of port) {
        allInRange = allInRange && portRange[0] * 1 <= p && p <= portRange[1] * 1;
        if (!allInRange) return false;
      }
    } else {
      return portRange[0] * 1 <= port && port <= portRange[1] * 1;
    }
  }
}

Policy.ARRAR_VALUE_KEYS = ["scope", "tag", "vpnProfile", "applyRules"];
Policy.INTF_PREFIX = "intf:";
Policy.TAG_PREFIX = "tag:";

module.exports = Policy
<|endoftext|>


<commit_before>volunteerService.controller('CalendarController', function($http, $scope, $compile, $timeout, uiCalendarConfig) {
        var date = new Date();
        var d = date.getDate();
        var m = date.getMonth();
        var y = date.getFullYear();

        $scope.date = date;
        $scope.newDate;
        $scope.year;
        $scope.month;
        $scope.day;
        $scope.viewMonth;
        $scope.organizationName;
        $scope.contactName;
        $scope.phoneNumber;
        $scope.contactEmail;
        $scope.numberOfVolunteers;
        $scope.typeOfServiceProject;

        /* event source that contains custom events on the scope */
        $scope.events = [];
        $scope.clickOnDate = function(date, jsEvent, view)
        {
            $scope.day = date.format('D');
            $scope.month = date.format('M');
            $scope.year = date.format('YYYY');
            $scope.newDate = new Date($scope.year, $scope.month, $scope.day);
            $scope.viewMonth = view.start._d.getMonth() + 2;
            if($scope.viewMonth == 13) {
                $scope.viewMonth = 1;
            }

            /* check if an event exists for the date clicked on */
            var eventExists = false;
            for(var k = 0; k<$scope.events.length; k++) {
                if($scope.day == $scope.events[k].day) {
                    eventExists = true;
                    break;
                }
            }
            if(!eventExists && $scope.month == $scope.viewMonth && ($scope.year >= $scope.date.getFullYear() && $scope.month >= $scope.date.getMonth() + 1 && $scope.day > $scope.date.getDate())) {
                $scope.openModal();
            }
        };

        /* add and removes an event source of choice */
        $scope.addRemoveEventSource = function(sources,source) {
            var canAdd = 0;
            angular.forEach(sources,function(value, key){
                if(sources[key] === source){
                    sources.splice(key,1);
                    canAdd = 1;
                }
            });
            if(canAdd === 0){
                sources.push(source);
            }
        };

        /* add custom event*/
        $scope.addEvent = function() {
            $scope.events.push({
                title: $scope.organizationName,
                start: new Date($scope.year, $scope.month - 1, $scope.day)
            });
        };
        /* remove event */
        $scope.remove = function(index) {
            $scope.events.splice(index,1);
        };
        /* Change View */
        $scope.changeView = function(view,calendar) {
            uiCalendarConfig.calendars[calendar].fullCalendar('changeView',view);
        };

        $scope.renderCalendar = function(calendar) {
            $timeout(function() {
                if(uiCalendarConfig.calendars[calendar]){
                    uiCalendarConfig.calendars[calendar].fullCalendar('render');
                }
            });
        };

        /* Render Tooltip */
        $scope.eventRender = function( event, element, view ) {
            element.attr({
                'tooltip': event.title,
                'tooltip-append-to-body': true
            });
            $compile(element)($scope);
        };

        $scope.retrieveMonthViewEvents = function(view) {
            $scope.getEventsMonth(view.start._d.getMonth() + 2, view.start._d.getFullYear());
        };

        $scope.loadMonthEvents = function(response) {
            for(var k = 0; k<$scope.events.length + 2; k++) {
                $scope.events.splice(0, 1);
            }
            for(var i = 0; i<response.data.length; i++) {
                $scope.events.push({
                    title: response.data[i].organization_name,
                    start: new Date(response.data[i].year, response.data[i].month - 1, response.data[i].day),
                    day: response.data[i].day,
                    month: response.data[i].month,
                    year: response.data[i].year,
                    contact_name: response.data[i].contactName,
                    phone_number: response.data[i].phoneNumber,
                    contact_email: response.data[i].contactEmail,
                    number_of_volunteers: response.data[i].numberOfVolunteers,
                    type_of_service_project: response.data[i].typeOfServiceProject,
                    color: '#a90329'
                });
            }
        };

        $scope.openModal= function(){
            $("#myModal").modal('show');
        };

        $scope.closeModal= function(){
            $("#myModal").modal('hide');
        };

        $scope.submitForm = function() {
            if ($scope.userForm.$valid) {
                $scope.postEvent();
                $scope.postEmail();
                $scope.getEventsMonth($scope.month, $scope.year);
                $scope.closeModal();
                window.location.reload();
            }
        };

        $scope.uiConfig = {
            calendar:{
                height: 550,
                editable: false,
                eventLimit: 1,
                header:{
                    left: 'title',
                    center: '',
                    right: 'today prev,next'
                },
                dayClick: $scope.clickOnDate,
                viewRender: $scope.retrieveMonthViewEvents,
                eventRender: $scope.eventRender
            }
        };

        $scope.getEventsMonth = function(month, year) {
            $http({
                method: 'GET',
                url: 'http://34.193.243.89:3000/service/' + month + '/' + year
            }).then(function successCallback(response){
                $scope.loadMonthEvents(response);
            }, function errorCallback(response) {
            });
        };

        $scope.postEvent = function() {
            $http({
                method: 'POST',
                url: 'http://34.193.243.89:3000/service/new',
                data: {
                    date: $scope.newDate,
                    year: $scope.year,
                    month: $scope.month,
                    day: $scope.day,
                    organization_name: $scope.organizationName,
                    contact_name: $scope.contactName,
                    phone_number: $scope.phoneNumber,
                    contact_email: $scope.contactEmail,
                    number_of_volunteers: $scope.numberOfVolunteers,
                    type_of_service_project: $scope.typeOfServiceProject
                }
            }).then(function successCallback(response){
                //console.log(response);
            }, function errorCallback(response) {
            });
        };

        $scope.postEmail = function() {
            $http({
                method: 'POST',
                url: 'http://34.193.243.89:3000/email/send',
                data: {
                    date: $scope.newDate,
                    year: $scope.year,
                    month: $scope.month,
                    day: $scope.day,
                    organization_name: $scope.organizationName,
                    contact_name: $scope.contactName,
                    phone_number: $scope.phoneNumber,
                    contact_email: $scope.contactEmail,
                    number_of_volunteers: $scope.numberOfVolunteers,
                    type_of_service_project: $scope.typeOfServiceProject
                }
            }).then(function successCallback(response){
                //console.log(response);
            }, function errorCallback(response) {
            });
        };

        /* event sources array*/
        $scope.eventSources = [$scope.events];
});<commit_msg>Fix preventing events from being scheduled on days before the day after the current day<commit_after>volunteerService.controller('CalendarController', function($http, $scope, $compile, $timeout, uiCalendarConfig) {
        var date = new Date();
        var d = date.getDate();
        var m = date.getMonth();
        var y = date.getFullYear();

        $scope.date = date;
        $scope.newDate;
        $scope.year;
        $scope.month;
        $scope.day;
        $scope.viewMonth;
        $scope.organizationName;
        $scope.contactName;
        $scope.phoneNumber;
        $scope.contactEmail;
        $scope.numberOfVolunteers;
        $scope.typeOfServiceProject;

        /* event source that contains custom events on the scope */
        $scope.events = [];
        $scope.clickOnDate = function(date, jsEvent, view)
        {
            $scope.day = date.format('D');
            $scope.month = date.format('M');
            $scope.year = date.format('YYYY');
            $scope.newDate = new Date($scope.year, $scope.month, $scope.day);
            $scope.viewMonth = view.start._d.getMonth() + 2;
            if($scope.viewMonth == 13) {
                $scope.viewMonth = 1;
            }

            /* check if an event exists for the date clicked on */
            var eventExists = false;
            for(var k = 0; k<$scope.events.length; k++) {
                if($scope.day == $scope.events[k].day) {
                    eventExists = true;
                    break;
                }
            }
            if(!eventExists && $scope.month == $scope.viewMonth && $scope.year >= $scope.date.getFullYear()) {
                if($scope.month == $scope.date.getMonth() + 1) {
                    if($scope.day > $scope.date.getDate()) {
                        $scope.openModal();
                    }
                }
                else if($scope.year == $scope.date.getFullYear()) {
                    if($scope.month > $scope.date.getMonth() + 1) {
                        $scope.openModal();
                    }
                }
                else {
                    $scope.openModal();
                }
            }
        };

        /* add and removes an event source of choice */
        $scope.addRemoveEventSource = function(sources,source) {
            var canAdd = 0;
            angular.forEach(sources,function(value, key){
                if(sources[key] === source){
                    sources.splice(key,1);
                    canAdd = 1;
                }
            });
            if(canAdd === 0){
                sources.push(source);
            }
        };

        /* add custom event*/
        $scope.addEvent = function() {
            $scope.events.push({
                title: $scope.organizationName,
                start: new Date($scope.year, $scope.month - 1, $scope.day)
            });
        };
        /* remove event */
        $scope.remove = function(index) {
            $scope.events.splice(index,1);
        };
        /* Change View */
        $scope.changeView = function(view,calendar) {
            uiCalendarConfig.calendars[calendar].fullCalendar('changeView',view);
        };

        $scope.renderCalendar = function(calendar) {
            $timeout(function() {
                if(uiCalendarConfig.calendars[calendar]){
                    uiCalendarConfig.calendars[calendar].fullCalendar('render');
                }
            });
        };

        /* Render Tooltip */
        $scope.eventRender = function( event, element, view ) {
            element.attr({
                'tooltip': event.title,
                'tooltip-append-to-body': true
            });
            $compile(element)($scope);
        };

        $scope.retrieveMonthViewEvents = function(view) {
            $scope.getEventsMonth(view.start._d.getMonth() + 2, view.start._d.getFullYear());
        };

        $scope.loadMonthEvents = function(response) {
            for(var k = 0; k<$scope.events.length + 2; k++) {
                $scope.events.splice(0, 1);
            }
            for(var i = 0; i<response.data.length; i++) {
                $scope.events.push({
                    title: response.data[i].organization_name,
                    start: new Date(response.data[i].year, response.data[i].month - 1, response.data[i].day),
                    day: response.data[i].day,
                    month: response.data[i].month,
                    year: response.data[i].year,
                    contact_name: response.data[i].contactName,
                    phone_number: response.data[i].phoneNumber,
                    contact_email: response.data[i].contactEmail,
                    number_of_volunteers: response.data[i].numberOfVolunteers,
                    type_of_service_project: response.data[i].typeOfServiceProject,
                    color: '#a90329'
                });
            }
        };

        $scope.openModal= function(){
            $("#myModal").modal('show');
        };

        $scope.closeModal= function(){
            $("#myModal").modal('hide');
        };

        $scope.submitForm = function() {
            if ($scope.userForm.$valid) {
                $scope.postEvent();
                $scope.postEmail();
                $scope.getEventsMonth($scope.month, $scope.year);
                $scope.closeModal();
                window.location.reload();
            }
        };

        $scope.uiConfig = {
            calendar:{
                height: 550,
                editable: false,
                eventLimit: 1,
                header:{
                    left: 'title',
                    center: '',
                    right: 'today prev,next'
                },
                dayClick: $scope.clickOnDate,
                viewRender: $scope.retrieveMonthViewEvents,
                eventRender: $scope.eventRender
            }
        };

        $scope.getEventsMonth = function(month, year) {
            $http({
                method: 'GET',
                url: 'http://34.193.243.89:3000/service/' + month + '/' + year
            }).then(function successCallback(response){
                $scope.loadMonthEvents(response);
            }, function errorCallback(response) {
            });
        };

        $scope.postEvent = function() {
            $http({
                method: 'POST',
                url: 'http://34.193.243.89:3000/service/new',
                data: {
                    date: $scope.newDate,
                    year: $scope.year,
                    month: $scope.month,
                    day: $scope.day,
                    organization_name: $scope.organizationName,
                    contact_name: $scope.contactName,
                    phone_number: $scope.phoneNumber,
                    contact_email: $scope.contactEmail,
                    number_of_volunteers: $scope.numberOfVolunteers,
                    type_of_service_project: $scope.typeOfServiceProject
                }
            }).then(function successCallback(response){
                //console.log(response);
            }, function errorCallback(response) {
            });
        };

        $scope.postEmail = function() {
            $http({
                method: 'POST',
                url: 'http://34.193.243.89:3000/email/send',
                data: {
                    date: $scope.newDate,
                    year: $scope.year,
                    month: $scope.month,
                    day: $scope.day,
                    organization_name: $scope.organizationName,
                    contact_name: $scope.contactName,
                    phone_number: $scope.phoneNumber,
                    contact_email: $scope.contactEmail,
                    number_of_volunteers: $scope.numberOfVolunteers,
                    type_of_service_project: $scope.typeOfServiceProject
                }
            }).then(function successCallback(response){
                //console.log(response);
            }, function errorCallback(response) {
            });
        };

        /* event sources array*/
        $scope.eventSources = [$scope.events];
});<|endoftext|>


<commit_before>function	evalhand(table, game, player)
{
	var		r;
	var		hand; // Example: Array("2s", "4d", "6h", "8c", "Tc");
	var		idx = 1;
	if (!player)
		var		player;
	var		j;

	while (idx <= 6)
	{
		if (get_seat(table.seats, idx).state == "playing")
		{
			player = get_seat(table.seats, idx).player;
			hand = game.board.slice(0);
			hand.push(player.card1);
			hand.push(player.card2);
			for (j = 0; j < hand.length; ++j)
			{
				if (hand[j][0] == "1")
					hand[j] = "A" + hand[j][1];
				else if (hand[j][0] == "k" || hand[j][0] == "q"
						|| hand[j][0] == "j" || hand[j][0] == "t")
					hand[j] = hand[j][0].toUpperCase() + hand[j][1];
			}
			r = texas.evaluate(hand);
			//console.log(hand);
			//console.log("SEAT" + idx + "->" + r.name);
			player.rank_name = r.name;
			player.rank_value = r.value;
			//console.log("Value: " + r.value);
			io.to(get_private_id(table.private_ids, idx)).emit("first suit", r.name);
		}
		++idx;
	}
}
<commit_msg>evalhand try<commit_after>function	evalhand(table, game, player)
{
	var		r;
	var		hand; // Example: Array("2s", "4d", "6h", "8c", "Tc");
	var		idx = 1;
//	if (!player)
//		var		player;
	var		j;

	while (idx <= 6)
	{
		if (get_seat(table.seats, idx).state == "playing")
		{
//			player = get_seat(table.seats, idx).player;
			hand = game.board.slice(0);
			console.log(hand);
			hand.push(player.card1);
			hand.push(player.card2);
			for (j = 0; j < hand.length; ++j)
			{
				if (hand[j][0] == "1")
					hand[j] = "A" + hand[j][1];
				else if (hand[j][0] == "k" || hand[j][0] == "q"
						|| hand[j][0] == "j" || hand[j][0] == "t")
					hand[j] = hand[j][0].toUpperCase() + hand[j][1];
			}
			r = texas.evaluate(hand);
			//console.log(hand);
			//console.log("SEAT" + idx + "->" + r.name);
			player.rank_name = r.name;
			player.rank_value = r.value;
			//console.log("Value: " + r.value);
			io.to(get_private_id(table.private_ids, idx)).emit("first suit", r.name);
		}
		++idx;
	}
}
<|endoftext|>


<commit_before>// Main code running in every mathdown document (all shown via index.html).

"use strict";

// Logging
// =======

// Prevent errors on IE (but do strive to log somehow if IE Dev Tools are open).
function log() {
  try {
    if(console.log.apply) {
      console.log.apply(console, arguments);
    } else {
      /* IE's console.log doesn't have .apply, .call, or bind. */
      console.log(Array.prototype.slice.call(arguments));
    }
  } catch (err) {}
}

// Send MathJax messages to log.
log("MJ:", MathJax.Message.Log());
var origFilterText = MathJax.Message.filterText;
MathJax.Message.filterText = function(text, n, msgType) {
  // Exclude non-informative "Processing/Typesetting math: 0% / 100%".
  if(msgType !== "ProcessMath" && msgType !== "TypesetMath") {
    log("MJ:", text, "[" + msgType + "]");
  }
  return origFilterText(text, n, msgType);
}

var statusElement = document.getElementById("status");
// Show "flash" message.  To hide use className="" but it's still
// useful to provide text (briefly visible).
function setStatus(className, text) {
  statusElement.innerHTML = "";
  statusElement.className = className;
  if(text) {
    statusElement.appendChild(document.createTextNode(text));
  }
}

// Random pad IDs
// ==============

// Return an int in [0, 62).
var random62 = (window.crypto && window.crypto.getRandomValues) ?
    function() {
      var buf = new Uint8Array(1);
      var n;
      do {
        window.crypto.getRandomValues(buf);
        n = (buf[0] % 64);
      } while(n >= 62);
      return n;
    }
    :
    function() {
      // Math.random is pathetic on most browsers, though some initialize
      // with secure seed: http://stackoverflow.com/a/18507748/239657
      return Math.floor(Math.random() * 62);
    };

function randomBase62(length) {
  var base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var text = "";
  for(var i=0; i < length; i++)
    text += base62.charAt(random62());
  return text;
}

function newPad() {
  var doc = randomBase62(11);  // 62**11 ~= 2**65.
  window.open("?doc=" + doc, "_blank");
}
window.newPad = newPad;

// Editor
// ======

// Apply class="leadingspace" to leading whitespace so we can make it monospace.
// Stolen from CodeMirror/addon/edit/trailingspace.js.
CodeMirror.defineOption("showLeadingSpace", false, function(cm, val, prev) {
  if (prev === CodeMirror.Init) prev = false;
  if (prev && !val)
    cm.removeOverlay("leadingspace");
  else if (!prev && val)
    cm.addOverlay({
      token: function(stream) {
        if (stream.sol() && stream.eatSpace()) {
          return "leadingspace";
        } else {
          stream.skipToEnd();
          return null;
        }
      },
      name: "leadingspace"
    });
});

// To add vertical margin to headers, the .cm-header[N] classes must apply to
// <pre> and not the <span>.  Wrap the mode to achieve this.
CodeMirror.defineMode("gfm_header_line_classes", function(cmConfig, modeCfg) {
  modeCfg.name = "gfm";
  modeCfg.highlightFormatting = true;
  var mode = CodeMirror.getMode(cmConfig, modeCfg);
  var origToken = mode.token;
  // We use GFM mostly for URLs but don't want GitHub-specific formatting that
  // makes `0123456789` a link.  TODO: add mode option of gfm.js.
  var shaOrIssueRE = /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40})|b^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+$/;
  mode.token = function(stream, state) {
    var classes = origToken(stream, state) || "";
    if (shaOrIssueRE.test(stream.current())) {
      classes = classes.replace(/(^| )link( |$)/, " ");
    }
    classes = classes.replace(/(^| )(header\S*)/g, "$1line-cm-$2");
    return /^\s*$/.test(classes) ? null : classes;
  }
  return mode;
});

// Defaults are insert \t / reindent.  Indent / dedent are more expected & useful.
CodeMirror.keyMap["default"]["Tab"] = "indentMore";
CodeMirror.keyMap["default"]["Shift-Tab"] = "indentLess";

var editor = CodeMirror.fromTextArea(document.getElementById("code"),
                                     {foldGutter: true,
                                      gutters: ["CodeMirror-foldgutter"],
                                      indentUnit: 4,
                                      lineNumbers: false,
                                      lineWrapping: true,
                                      mode: "gfm_header_line_classes",
                                      showLeadingSpace: true,
                                      direction: docDirection});

// Indent soft-wrapped lines.  Based on CodeMirror/demo/indentwrap.html.
var leadingSpaceListBulletsQuotes = /^\s*([*+-]\s+|\d+\.\s+|>\s*)*/;
editor.on("renderLine", function(cm, line, elt) {
  // Show continued list/qoute lines aligned to start of text rather
  // than first non-space char.  MINOR BUG: also does this inside
  // literal blocks.
  // Would like to measure real sizes of spans styled
  // /leadingspace|formatting-list|formatting-quote/, but can't do
  // that without inserting them into the DOM.  So count chars instead.
  var leading = (leadingSpaceListBulletsQuotes.exec(line.text) || [""])[0];
  var off = CodeMirror.countColumn(leading, leading.length, cm.getOption("tabSize"));
  // Using "ex" is a bit better than cm.defaultCharWidth() — it picks up
  // increased font if applied to whole line, i.e. in header lines
  // (not that it makes sense to indent headers).
  // However any resemblance of 1ex to the width of one monospace char
  // is purely coincidental (1em is way too wide in practice).
  elt.style.textIndent = "-" + off + "ex";

  // We need to know line direction to set paddingLeft or paddingRight appropriately.
  // TODO: CM should expose getDirection().
  var lineDirection = elt.style.direction || cm.getDoc().direction;
  // TODO: CM doesn't re-run this hook when line direction changed :-(
  // => KLUDGE: set both paddings!  In fact this is a good for mixed-direction docs anyway -
  //    long lines of opposite direction won't visually break indentation structrure.
  //    But I'm afraid it may be a nuisance on pure-LTR docs, so as a compromise
  //    I set only paddingLeft on LTR lines in LTR docs.
  if (docDirection === "rtl" || lineDirection === "rtl") {
    elt.style.paddingRight = off + "ex";
    elt.style.paddingLeft = off + "ex";
  } else {
    elt.style.paddingLeft = off + "ex";
  }
});
editor.refresh();

// Keep title and url #hash part in sync with first line of document.
function updateTitle() {
  var text = editor.getLine(0); // TODO: find first # Title line?
  text = text.replace(/^\s*#+\s*/, "");
  document.title = text + " | mathdown";
  window.location.hash = text.replace(/\W+/g, "-").replace(/^-/, "").replace(/-$/, "");
}
CodeMirror.on(editor.getDoc(), "change", function(doc, changeObj) {
  if (changeObj.from.line === 0) {
    updateTitle();
  }
});

CodeMirror.hookMath(editor, MathJax);

// Firepad
// =======

function setupFirepad(editor, firepad) {
  firepad.on("ready", function() {
    setStatus("", "done");

    if (firepad.isHistoryEmpty()) {
      firepad.setText(
        "# Untitled\n" +
          "Bookmark this page — the random-looking address is the only way to access this document!\n" +
          "Share the link to collaborate.\n" +
          "Edits are saved in real time.\n" +
          "\n" +
          "Use Markdown for document structure and $\\LaTeX$ math syntax (see **Help** link above).\n" +
          "To edit formulas just enter them with arrow keys.\n" +
          // HACK: Pad the editor with a few empty lines so it looks
          // more inviting to write in.
          "\n" +
          "\n"
      );
    } else {
      // Keeping cursor at start (hopefully) reduces CM redrawing as
      // syntax highlight and math rendering changes line heights.
      editor.setCursor({line: 0, ch: 0});
    }

    // CM's autofocus option doesn't work with Firepad - seems focused
    // but can't type.  Focusing here after Firepad init works.
    editor.focus();

    // Queuing this allows text to appear before math.
    MathJax.Hub.Queue(function() {
      editor.renderAllMath();
    });
  });

  //  firepadRef.child("
  firepad.on("synced", function(isSynced) {
    log("synced", isSynced);
    if(!isSynced) {
      setStatus("info", "Unsaved!");
    } else {
      setStatus("", "saved");
    }
  })
}

// URL parameters
// ==============

function locationQueryParams() {
  /* If more is needed, use https://github.com/medialize/URI.js */
  var queryParts = window.location.search.replace(/^\?/g, "").split("&");
  var params = {};
  for(var i = 0; i < queryParts.length; i++) {
    var keyval = queryParts[i].split("=");
    params[decodeURIComponent(keyval[0])] = decodeURIComponent(keyval[1] || "");
  }
  return params;
}

var queryParams = locationQueryParams();
var doc = queryParams["doc"];
// EXPERIMENTAL KLUDGE param: for now we support dir=rtl to make RTL docs (somewhat) practical
// (but don't expose it in the GUI).
// In the future it might be ignored - once we autodetect each line's base direction (#23).
// Also, document direction is semantic, it makes more sense to store it in firebase?
var docDirection = (queryParams["dir"] === "rtl" ? "rtl" : "ltr");

if(doc === undefined) {
    // TODO: this should be a server-side redirect (when we have a server).
    window.location.search = "?doc=about";
} else {
  var rootRef = new Firebase("https://mathdown.firebaseIO.com/");
  var firepadsRef = rootRef.child("firepads");
  var firepadRef = firepadsRef.child(doc);
  log("firebase ref:", firepadRef.toString());

  var firepad = Firepad.fromCodeMirror(firepadRef, editor);
  setupFirepad(editor, firepad);
}
<commit_msg>Refactor editor creation to a function as well.<commit_after>// Main code running in every mathdown document (all shown via index.html).

"use strict";

// Logging
// =======

// Prevent errors on IE (but do strive to log somehow if IE Dev Tools are open).
function log() {
  try {
    if(console.log.apply) {
      console.log.apply(console, arguments);
    } else {
      /* IE's console.log doesn't have .apply, .call, or bind. */
      console.log(Array.prototype.slice.call(arguments));
    }
  } catch (err) {}
}

// Send MathJax messages to log.
log("MJ:", MathJax.Message.Log());
var origFilterText = MathJax.Message.filterText;
MathJax.Message.filterText = function(text, n, msgType) {
  // Exclude non-informative "Processing/Typesetting math: 0% / 100%".
  if(msgType !== "ProcessMath" && msgType !== "TypesetMath") {
    log("MJ:", text, "[" + msgType + "]");
  }
  return origFilterText(text, n, msgType);
}

var statusElement = document.getElementById("status");
// Show "flash" message.  To hide use className="" but it's still
// useful to provide text (briefly visible).
function setStatus(className, text) {
  statusElement.innerHTML = "";
  statusElement.className = className;
  if(text) {
    statusElement.appendChild(document.createTextNode(text));
  }
}

// Random pad IDs
// ==============

// Return an int in [0, 62).
var random62 = (window.crypto && window.crypto.getRandomValues) ?
    function() {
      var buf = new Uint8Array(1);
      var n;
      do {
        window.crypto.getRandomValues(buf);
        n = (buf[0] % 64);
      } while(n >= 62);
      return n;
    }
    :
    function() {
      // Math.random is pathetic on most browsers, though some initialize
      // with secure seed: http://stackoverflow.com/a/18507748/239657
      return Math.floor(Math.random() * 62);
    };

function randomBase62(length) {
  var base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var text = "";
  for(var i=0; i < length; i++)
    text += base62.charAt(random62());
  return text;
}

function newPad() {
  var doc = randomBase62(11);  // 62**11 ~= 2**65.
  window.open("?doc=" + doc, "_blank");
}
window.newPad = newPad;

// Editor
// ======

// Apply class="leadingspace" to leading whitespace so we can make it monospace.
// Stolen from CodeMirror/addon/edit/trailingspace.js.
CodeMirror.defineOption("showLeadingSpace", false, function(cm, val, prev) {
  if (prev === CodeMirror.Init) prev = false;
  if (prev && !val)
    cm.removeOverlay("leadingspace");
  else if (!prev && val)
    cm.addOverlay({
      token: function(stream) {
        if (stream.sol() && stream.eatSpace()) {
          return "leadingspace";
        } else {
          stream.skipToEnd();
          return null;
        }
      },
      name: "leadingspace"
    });
});

// To add vertical margin to headers, the .cm-header[N] classes must apply to
// <pre> and not the <span>.  Wrap the mode to achieve this.
CodeMirror.defineMode("gfm_header_line_classes", function(cmConfig, modeCfg) {
  modeCfg.name = "gfm";
  modeCfg.highlightFormatting = true;
  var mode = CodeMirror.getMode(cmConfig, modeCfg);
  var origToken = mode.token;
  // We use GFM mostly for URLs but don't want GitHub-specific formatting that
  // makes `0123456789` a link.  TODO: add mode option of gfm.js.
  var shaOrIssueRE = /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40})|b^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+$/;
  mode.token = function(stream, state) {
    var classes = origToken(stream, state) || "";
    if (shaOrIssueRE.test(stream.current())) {
      classes = classes.replace(/(^| )link( |$)/, " ");
    }
    classes = classes.replace(/(^| )(header\S*)/g, "$1line-cm-$2");
    return /^\s*$/.test(classes) ? null : classes;
  }
  return mode;
});

// Defaults are insert \t / reindent.  Indent / dedent are more expected & useful.
CodeMirror.keyMap["default"]["Tab"] = "indentMore";
CodeMirror.keyMap["default"]["Shift-Tab"] = "indentLess";

function createEditor(docDirection) {
  return CodeMirror.fromTextArea(document.getElementById("code"),
                                 {foldGutter: true,
                                  gutters: ["CodeMirror-foldgutter"],
                                  indentUnit: 4,
                                  lineNumbers: false,
                                  lineWrapping: true,
                                  mode: "gfm_header_line_classes",
                                  showLeadingSpace: true,
                                  direction: docDirection});
}

function setupEditor(editor) {
  // Indent soft-wrapped lines.  Based on CodeMirror/demo/indentwrap.html.
  var leadingSpaceListBulletsQuotes = /^\s*([*+-]\s+|\d+\.\s+|>\s*)*/;
  editor.on("renderLine", function(cm, line, elt) {
    // Show continued list/qoute lines aligned to start of text rather
    // than first non-space char.  MINOR BUG: also does this inside
    // literal blocks.
    // Would like to measure real sizes of spans styled
    // /leadingspace|formatting-list|formatting-quote/, but can't do
    // that without inserting them into the DOM.  So count chars instead.
    var leading = (leadingSpaceListBulletsQuotes.exec(line.text) || [""])[0];
    var off = CodeMirror.countColumn(leading, leading.length, cm.getOption("tabSize"));
    // Using "ex" is a bit better than cm.defaultCharWidth() — it picks up
    // increased font if applied to whole line, i.e. in header lines
    // (not that it makes sense to indent headers).
    // However any resemblance of 1ex to the width of one monospace char
    // is purely coincidental (1em is way too wide in practice).
    elt.style.textIndent = "-" + off + "ex";

    // We need to know line direction to set paddingLeft or paddingRight appropriately.
    // TODO: CM should expose getDirection().
    var lineDirection = elt.style.direction || cm.getDoc().direction;
    // TODO: CM doesn't re-run this hook when line direction changed :-(
    // => KLUDGE: set both paddings!  In fact this is a good for mixed-direction docs anyway -
    //    long lines of opposite direction won't visually break indentation structrure.
    //    But I'm afraid it may be a nuisance on pure-LTR docs, so as a compromise
    //    I set only paddingLeft on LTR lines in LTR docs.
    if (docDirection === "rtl" || lineDirection === "rtl") {
      elt.style.paddingRight = off + "ex";
      elt.style.paddingLeft = off + "ex";
    } else {
      elt.style.paddingLeft = off + "ex";
    }
  });
  editor.refresh();

  // Keep title and url #hash part in sync with first line of document.
  function updateTitle() {
    var text = editor.getLine(0); // TODO: find first # Title line?
    text = text.replace(/^\s*#+\s*/, "");
    document.title = text + " | mathdown";
    window.location.hash = text.replace(/\W+/g, "-").replace(/^-/, "").replace(/-$/, "");
  }
  CodeMirror.on(editor.getDoc(), "change", function(doc, changeObj) {
    if (changeObj.from.line === 0) {
      updateTitle();
    }
  });

  CodeMirror.hookMath(editor, MathJax);
}

// Firepad
// =======

function setupFirepad(editor, firepad) {
  firepad.on("ready", function() {
    setStatus("", "done");

    if (firepad.isHistoryEmpty()) {
      firepad.setText(
        "# Untitled\n" +
          "Bookmark this page — the random-looking address is the only way to access this document!\n" +
          "Share the link to collaborate.\n" +
          "Edits are saved in real time.\n" +
          "\n" +
          "Use Markdown for document structure and $\\LaTeX$ math syntax (see **Help** link above).\n" +
          "To edit formulas just enter them with arrow keys.\n" +
          // HACK: Pad the editor with a few empty lines so it looks
          // more inviting to write in.
          "\n" +
          "\n"
      );
    } else {
      // Keeping cursor at start (hopefully) reduces CM redrawing as
      // syntax highlight and math rendering changes line heights.
      editor.setCursor({line: 0, ch: 0});
    }

    // CM's autofocus option doesn't work with Firepad - seems focused
    // but can't type.  Focusing here after Firepad init works.
    editor.focus();

    // Queuing this allows text to appear before math.
    MathJax.Hub.Queue(function() {
      editor.renderAllMath();
    });
  });

  //  firepadRef.child("
  firepad.on("synced", function(isSynced) {
    log("synced", isSynced);
    if(!isSynced) {
      setStatus("info", "Unsaved!");
    } else {
      setStatus("", "saved");
    }
  })
}

// URL parameters => load document
// ===============================

function locationQueryParams() {
  /* If more is needed, use https://github.com/medialize/URI.js */
  var queryParts = window.location.search.replace(/^\?/g, "").split("&");
  var params = {};
  for(var i = 0; i < queryParts.length; i++) {
    var keyval = queryParts[i].split("=");
    params[decodeURIComponent(keyval[0])] = decodeURIComponent(keyval[1] || "");
  }
  return params;
}

var queryParams = locationQueryParams();
var doc = queryParams["doc"];
// EXPERIMENTAL KLUDGE param: for now we support dir=rtl to make RTL docs (somewhat) practical
// (but don't expose it in the GUI).
// In the future it might be ignored - once we autodetect each line's base direction (#23).
// Also, document direction is semantic, it makes more sense to store it in firebase?
var docDirection = (queryParams["dir"] === "rtl" ? "rtl" : "ltr");

if(doc === undefined) {
    // TODO: this should be a server-side redirect (when we have a server).
    window.location.search = "?doc=about";
} else {
  var rootRef = new Firebase("https://mathdown.firebaseIO.com/");
  var firepadsRef = rootRef.child("firepads");
  var firepadRef = firepadsRef.child(doc);
  log("firebase ref:", firepadRef.toString());

  var editor = createEditor(docDirection);
  setupEditor(editor);

  var firepad = Firepad.fromCodeMirror(firepadRef, editor);
  setupFirepad(editor, firepad);
}
<|endoftext|>


<commit_before><commit_msg>ci(e2e): properly exit log uploader if not configured properly<commit_after><|endoftext|>


<commit_before>/* globals bufferpm, chrome */

// Put together a query string for the iframe
var buildSrc = function(data, config) {

  var src = data.local ?
    config.overlay.localendpoint :
    config.overlay.endpoint;

  var qs = '';

  config.attributes.forEach(function(attr, i){
    if ( !data[ attr.name ] ) return;
    if ( qs.length ) qs += '&';
    qs += attr.name + '=' + attr.encode( data[attr.name] );
  });

  if (qs.length) src += '?' + qs;

  return src;
};

var openPopUp = function(src, doneCallback) {

  window.open(src, null, 'height=600,width=850');

  // Bind close listener
  // Listen for when the overlay has closed itself
  bufferpm.bind("buffermessage", function(overlaydata) {
    bufferpm.unbind("buffermessage");
    setTimeout(function () {
      doneCallback(overlaydata);
    }, 0);
    window.focus();
  });
};

/**
 * This is a list of domains that have a strict Content Security Policy
 * This is a temporary work around while we figure out a long term solution
 * to this issue: https://github.com/bufferapp/buffer-chrome/issues/12
 */
var CSPWhitelist = [
  'github.com',
  'education.github.com',
  'medium.com',
  'www.npmjs.org',
  'www.npmjs.com' // DEC 2014
];

// Build that overlay!
// Triggered by code working from the button up
var bufferOverlay = function(data, config, doneCallback) {

  if( ! doneCallback ) doneCallback = function () {};
  if( ! config ) return;

  var src = buildSrc(data, config);
  var domain = window.location.hostname;

  if (xt.options['buffer.op.tpc-disabled'] ||
    CSPWhitelist.indexOf(domain) > -1 ) {
    return openPopUp(src, doneCallback);
  }

  // Create the iframe and add the footer:
  var iframe = document.createElement('iframe');

  iframe.allowtransparency = 'true';
  iframe.scrolling = 'no';
  iframe.id = 'buffer_overlay';
  iframe.name = 'buffer_overlay';
  iframe.style.cssText = config.overlay.getCSS();

  iframe.src = src;

  // Testing CSP issues w/ sandboxed iframe
  // var qs = src.split('?')[1];
  // iframe.src = xt.data.get('data/shared/extension.html?' + qs);

  document.body.appendChild(iframe);

  var styleTag = createStyleTag();
  document.body.appendChild(styleTag);

  var dashboardButton = createDashboardButton();
  document.body.appendChild(dashboardButton);

  var cancelButton = createCancelButton();
  document.body.appendChild(cancelButton);

  $(document).on('click', '.buffer-floating-cancel-btn', function() {
      closePopup(document, doneCallback);
  });

  // Bind close listener
  // Listen for when the overlay has closed itself
  bufferpm.bind('buffermessage', function(overlaydata) {
      closePopup(document, doneCallback, overlaydata);
  });

  /**
   * Listen to ESC key and close the popup when hit.
   */
  $(document).keyup(function(e) {
    if (e.keyCode == 27) {
        closePopup(document, doneCallback);
    }
  });

  // Remove the loading image when we hear from the other side
  bufferpm.bind('buffer_loaded', function(data) {
    bufferpm.unbind('buffer_loaded');
    iframe.style.backgroundImage = 'none';
  });
};

function closePopup(document, doneCallback, overlayData) {

    if ($("#buffer_overlay")) {
        $("#buffer_overlay").remove();
    }

    if ($(".buffer-floating-cancel-btn")) {
        $(".buffer-floating-cancel-btn").remove();
    }

    if ($(".buffer-floating-btn")) {
        $(".buffer-floating-btn").remove();
    }

    bufferpm.unbind('buffermessage');
    bufferpm.unbind('buffer_addbutton');

    setTimeout(function () {
      doneCallback(overlayData);
    }, 0);

    window.focus();
}

var createStyleTag = function() {
  var style = document.createElement('style');
  style.type = 'text/css';

  var content = document.createTextNode([
    '.buffer-floating-btn:hover {',
      'text-decoration: none;',
      'color: #323b43;',
      'cursor: pointer;',
    '}'
  ].join(''));

  style.appendChild(content);

  return style;
};

var createDashboardButton = function() {

  var css = [
    'position: fixed;',
    'top: 10px;',
    'right: 10px;',
    'z-index: 2147483647;',
    'padding: 8px 10px 8px 32px;',
    'background-color: #fff;',
    'background-image: url(https://d389zggrogs7qo.cloudfront.net/images/bookmarklet_icon.png);',
    'background-repeat: no-repeat;',
    'background-size: 15px;',
    'background-position: 11px 12px;',
    'color: #323b43;',
    'border: 0;',
    'text-decoration: none;',
    'border-radius: 2px;',
    'text-decoration: none;',
    'font-size: 14px;',
    'line-height: 1.6;',
    'font-family: "Open Sans", Roboto, Helvetica, Arial;',
    'box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);'
  ].join('');

  var button = document.createElement('a');
  button.href = 'https://bufferapp.com/app';
  button.target = '_blank';
  button.setAttribute('class', 'buffer-floating-btn');
  button.setAttribute('style', css);

  var text = document.createTextNode('Go to Buffer');
  button.appendChild(text);

  return button;
};

var createCancelButton = function() {

  var css = [
    'position: fixed;',
    'top: 10px;',
    'left: 10px;',
    'z-index: 2147483647;',
    'padding: 8px 10px 8px 32px;',
    'background-color: #fff;',
    'background-image: url(https://d389zggrogs7qo.cloudfront.net/images/bookmarklet_icon.png);',
    'background-repeat: no-repeat;',
    'background-size: 15px;',
    'background-position: 11px 12px;',
    'color: #323b43;',
    'border: 0;',
    'text-decoration: none;',
    'border-radius: 2px;',
    'text-decoration: none;',
    'font-size: 14px;',
    'line-height: 1.6;',
    'font-family: "Open Sans", Roboto, Helvetica, Arial;',
    'box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);'
  ].join('');

  var button = document.createElement('button');
  button.setAttribute('class', 'buffer-floating-btn buffer-floating-cancel-btn');
  button.setAttribute('style', css);

  var text = document.createTextNode('Cancel');
  button.appendChild(text);

  return button;
};

// getOverlayConfig returns the configuration object for use by bufferOverlay
var getOverlayConfig = function(postData){

  var config = {};

  // Set this to true for using a local server while testing
  config.local = false;

  var segments = window.location.pathname.split('/');

  config.pocketWeb = ( window.location.host.indexOf('getpocket') !== -1 && segments[2] === 'read' );

  // Specification for gathering data for the overlay
  config.attributes = [
    {
      name: "url",
      get: function (cb) {
        if(config.pocketWeb){
          var li = document.getElementsByClassName('original')[0];
          var link = li.getElementsByTagName('a')[0].href;
          cb(link);
        }
        else{
          cb(window.location.href);
        }
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "text",
      get: function (cb) {
        if(document.getSelection().toString().length) {
          cb('"' + document.getSelection().toString() + '"');
        }
        else{
          if(config.pocketWeb){
            var headline = document.querySelectorAll('.reader_head h1')[0];
            var title = headline && headline.textContent;
            cb(title);
          }
          else{
            cb(document.title);
          }
        }
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_tweet_id",
      get: function (cb) {
        cb(postData.retweeted_tweet_id);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_id",
      get: function (cb) {
        cb(postData.retweeted_user_id);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_name",
      get: function (cb) {
        cb(postData.retweeted_user_name);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_display_name",
      get: function (cb) {
        cb(postData.retweeted_user_display_name);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "picture",
      get: function (cb) {
        cb(postData.image);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "embed",
      get: function (cb) {
        cb(postData.embed);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "local",
      get: function (cb) {
        cb(config.local);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "version",
      get: function (cb) {
        cb(postData.version);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "placement",
      get: function (cb) {
        if( postData.placement ) cb(postData.placement);
        else cb('general');
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    }
  ];

  var loadingImgRel = 'img/white-loading-gif-small.gif';
  var loadingImg = typeof chrome !== 'undefined' ?
    chrome.extension.getURL('data/shared/' +  loadingImgRel) :
    document.location.protocol === 'http:' ?
      'http://static.bufferapp.com/images/extensions/' + loadingImgRel :
      'https://d389zggrogs7qo.cloudfront.net/images/extensions/' + loadingImgRel;

  config.overlay = {
    endpoint: "https://bufferapp.com/add/",
    localendpoint: "https://local.bufferapp.com/add/",
    getCSS: function () {
      return [
        'border: none;',
        'height: 100%;',
        'width: 100%;',
        'position: fixed!important;',
        'z-index: 2147483646;',
        'top:0;',
        'left:0;',
        'display: block!important;',
        'max-width: 100%!important;',
        'max-height: 100%!important;',
        'padding: 0!important;',
        'background: rgba(0, 0, 0, 0.1) url(' + loadingImg +') no-repeat center center;',
        'background-size: 40px;'
      ].join('');
    }
  };

  return config;
};

// Method for handling the async firing of the cb
var executeAfter = function(done, count, args, cb) {
  if(done === count) {
    setTimeout(function(){
      cb.apply( null, args );
    }, 0);
  }
};

// Asynchronously gather data about the page and from embedded sources,
// like Twitter or Facebook. Currently the async is a bit over the top,
// and not used, but if we need async down the line, it's there.
var getData = function (postData, cb) {
  var config = getOverlayConfig( postData );
  var count = config.attributes.length;
  var done = 0;
  var data = {};

  config.attributes.forEach(function(attr, i){
    attr.get(function(d){
      done += 1;
      data[ attr.name ] = d;
      executeAfter(done, count, [ data, config ], cb);
    });
  });
};

// bufferData is triggered by the buffer_click listener in
// the buffer-browser-embed file, where it's passed a port
// to communicate with the extension and data sent from the
// background page.
var bufferData = function (port, postData) {

  if (window.top !== window) return;

  // Transform the data somewhat, and then create an overlay.
  // When it's done, fire buffer_done back to the extension
  var createOverlay = function (data, config) {
    if( data.embed ) {
      if( typeof data.embed === "object" ) {
        for( var i in data.embed ) {
          if( data.embed.hasOwnProperty(i) ) {
            data[i] = data.embed[i];
          }
        }
        if( data.embed.text && !data.embed.url ) {
          data.url = null;
        }
        data.embed = null;
      } else {
        data.text = data.embed;
        data.url = null;
        data.embed = null;
      }
    }
    bufferOverlay(data, config, function (overlaydata) {
      port.emit("buffer_done", overlaydata);
    });
  };

  // It all starts here.
  // createOverlay is the callback that should fire after getData has
  // gathered all the necessaries
  getData(postData, createOverlay);
};
<commit_msg>Removed the icon from Cancel button and also centered it.<commit_after>/* globals bufferpm, chrome */

// Put together a query string for the iframe
var buildSrc = function(data, config) {

  var src = data.local ?
    config.overlay.localendpoint :
    config.overlay.endpoint;

  var qs = '';

  config.attributes.forEach(function(attr, i){
    if ( !data[ attr.name ] ) return;
    if ( qs.length ) qs += '&';
    qs += attr.name + '=' + attr.encode( data[attr.name] );
  });

  if (qs.length) src += '?' + qs;

  return src;
};

var openPopUp = function(src, doneCallback) {

  window.open(src, null, 'height=600,width=850');

  // Bind close listener
  // Listen for when the overlay has closed itself
  bufferpm.bind("buffermessage", function(overlaydata) {
    bufferpm.unbind("buffermessage");
    setTimeout(function () {
      doneCallback(overlaydata);
    }, 0);
    window.focus();
  });
};

/**
 * This is a list of domains that have a strict Content Security Policy
 * This is a temporary work around while we figure out a long term solution
 * to this issue: https://github.com/bufferapp/buffer-chrome/issues/12
 */
var CSPWhitelist = [
  'github.com',
  'education.github.com',
  'medium.com',
  'www.npmjs.org',
  'www.npmjs.com' // DEC 2014
];

// Build that overlay!
// Triggered by code working from the button up
var bufferOverlay = function(data, config, doneCallback) {

  if( ! doneCallback ) doneCallback = function () {};
  if( ! config ) return;

  var src = buildSrc(data, config);
  var domain = window.location.hostname;

  if (xt.options['buffer.op.tpc-disabled'] ||
    CSPWhitelist.indexOf(domain) > -1 ) {
    return openPopUp(src, doneCallback);
  }

  // Create the iframe and add the footer:
  var iframe = document.createElement('iframe');

  iframe.allowtransparency = 'true';
  iframe.scrolling = 'no';
  iframe.id = 'buffer_overlay';
  iframe.name = 'buffer_overlay';
  iframe.style.cssText = config.overlay.getCSS();

  iframe.src = src;

  // Testing CSP issues w/ sandboxed iframe
  // var qs = src.split('?')[1];
  // iframe.src = xt.data.get('data/shared/extension.html?' + qs);

  document.body.appendChild(iframe);

  var styleTag = createStyleTag();
  document.body.appendChild(styleTag);

  var dashboardButton = createDashboardButton();
  document.body.appendChild(dashboardButton);

  var cancelButton = createCancelButton();
  document.body.appendChild(cancelButton);

  $(document).on('click', '.buffer-floating-cancel-btn', function() {
      closePopup(document, doneCallback);
  });

  // Bind close listener
  // Listen for when the overlay has closed itself
  bufferpm.bind('buffermessage', function(overlaydata) {
      closePopup(document, doneCallback, overlaydata);
  });

  /**
   * Listen to ESC key and close the popup when hit.
   */
  $(document).keyup(function(e) {
    if (e.keyCode == 27) {
        closePopup(document, doneCallback);
    }
  });

  // Remove the loading image when we hear from the other side
  bufferpm.bind('buffer_loaded', function(data) {
    bufferpm.unbind('buffer_loaded');
    iframe.style.backgroundImage = 'none';

    if ($(".buffer-floating-cancel-btn").length) {
        $(".buffer-floating-cancel-btn").remove();
    }
  });
};

function closePopup(document, doneCallback, overlayData) {

    if ($("#buffer_overlay").length) {
        $("#buffer_overlay").remove();
    }

    if ($(".buffer-floating-cancel-btn").length) {
        $(".buffer-floating-cancel-btn").remove();
    }

    if ($(".buffer-floating-btn").length) {
        $(".buffer-floating-btn").remove();
    }

    bufferpm.unbind('buffermessage');
    bufferpm.unbind('buffer_addbutton');

    setTimeout(function () {
      doneCallback(overlayData);
    }, 0);

    window.focus();
}

var createStyleTag = function() {
  var style = document.createElement('style');
  style.type = 'text/css';

  var content = document.createTextNode([
    '.buffer-floating-btn:hover {',
      'text-decoration: none;',
      'color: #323b43;',
      'cursor: pointer;',
    '}'
  ].join(''));

  style.appendChild(content);

  return style;
};

var createDashboardButton = function() {

  var css = [
    'position: fixed;',
    'top: 10px;',
    'right: 10px;',
    'z-index: 2147483647;',
    'padding: 8px 10px 8px 32px;',
    'background-color: #fff;',
    'background-image: url(https://d389zggrogs7qo.cloudfront.net/images/bookmarklet_icon.png);',
    'background-repeat: no-repeat;',
    'background-size: 15px;',
    'background-position: 11px 12px;',
    'color: #323b43;',
    'border: 0;',
    'text-decoration: none;',
    'border-radius: 2px;',
    'text-decoration: none;',
    'font-size: 14px;',
    'line-height: 1.6;',
    'font-family: "Open Sans", Roboto, Helvetica, Arial;',
    'box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);'
  ].join('');

  var button = document.createElement('a');
  button.href = 'https://bufferapp.com/app';
  button.target = '_blank';
  button.setAttribute('class', 'buffer-floating-btn');
  button.setAttribute('style', css);

  var text = document.createTextNode('Go to Buffer');
  button.appendChild(text);

  return button;
};

var createCancelButton = function() {

  var css = [
    'position: fixed;',
    'top: 10px;',
    'left: 10px;',
    'z-index: 2147483647;',
    'padding: 8px 10px 8px 10px;',
    'background-color: #fff;',
    'background-repeat: no-repeat;',
    'color: #323b43;',
    'border: 0;',
    'text-decoration: none;',
    'border-radius: 2px;',
    'text-decoration: none;',
    'font-size: 14px;',
    'line-height: 1.6;',
    'font-family: "Open Sans", Roboto, Helvetica, Arial;',
    'box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);'
  ].join('');

  var button = document.createElement('button');
  button.setAttribute('class', 'buffer-floating-btn buffer-floating-cancel-btn');
  button.setAttribute('style', css);

  var text = document.createTextNode('Cancel');
  button.appendChild(text);

  return button;
};

// getOverlayConfig returns the configuration object for use by bufferOverlay
var getOverlayConfig = function(postData){

  var config = {};

  // Set this to true for using a local server while testing
  config.local = false;

  var segments = window.location.pathname.split('/');

  config.pocketWeb = ( window.location.host.indexOf('getpocket') !== -1 && segments[2] === 'read' );

  // Specification for gathering data for the overlay
  config.attributes = [
    {
      name: "url",
      get: function (cb) {
        if(config.pocketWeb){
          var li = document.getElementsByClassName('original')[0];
          var link = li.getElementsByTagName('a')[0].href;
          cb(link);
        }
        else{
          cb(window.location.href);
        }
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "text",
      get: function (cb) {
        if(document.getSelection().toString().length) {
          cb('"' + document.getSelection().toString() + '"');
        }
        else{
          if(config.pocketWeb){
            var headline = document.querySelectorAll('.reader_head h1')[0];
            var title = headline && headline.textContent;
            cb(title);
          }
          else{
            cb(document.title);
          }
        }
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_tweet_id",
      get: function (cb) {
        cb(postData.retweeted_tweet_id);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_id",
      get: function (cb) {
        cb(postData.retweeted_user_id);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_name",
      get: function (cb) {
        cb(postData.retweeted_user_name);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "retweeted_user_display_name",
      get: function (cb) {
        cb(postData.retweeted_user_display_name);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "picture",
      get: function (cb) {
        cb(postData.image);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "embed",
      get: function (cb) {
        cb(postData.embed);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "local",
      get: function (cb) {
        cb(config.local);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "version",
      get: function (cb) {
        cb(postData.version);
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    },
    {
      name: "placement",
      get: function (cb) {
        if( postData.placement ) cb(postData.placement);
        else cb('general');
      },
      encode: function (val) {
        return encodeURIComponent(val);
      }
    }
  ];

  var loadingImgRel = 'img/white-loading-gif-small.gif';
  var loadingImg = typeof chrome !== 'undefined' ?
    chrome.extension.getURL('data/shared/' +  loadingImgRel) :
    document.location.protocol === 'http:' ?
      'http://static.bufferapp.com/images/extensions/' + loadingImgRel :
      'https://d389zggrogs7qo.cloudfront.net/images/extensions/' + loadingImgRel;

  config.overlay = {
    endpoint: "https://bufferapp.com/add/",
    localendpoint: "https://local.bufferapp.com/add/",
    getCSS: function () {
      return [
        'border: none;',
        'height: 100%;',
        'width: 100%;',
        'position: fixed!important;',
        'z-index: 2147483646;',
        'top:0;',
        'left:0;',
        'display: block!important;',
        'max-width: 100%!important;',
        'max-height: 100%!important;',
        'padding: 0!important;',
        'background: rgba(0, 0, 0, 0.1) url(' + loadingImg +') no-repeat center center;',
        'background-size: 40px;'
      ].join('');
    }
  };

  return config;
};

// Method for handling the async firing of the cb
var executeAfter = function(done, count, args, cb) {
  if(done === count) {
    setTimeout(function(){
      cb.apply( null, args );
    }, 0);
  }
};

// Asynchronously gather data about the page and from embedded sources,
// like Twitter or Facebook. Currently the async is a bit over the top,
// and not used, but if we need async down the line, it's there.
var getData = function (postData, cb) {
  var config = getOverlayConfig( postData );
  var count = config.attributes.length;
  var done = 0;
  var data = {};

  config.attributes.forEach(function(attr, i){
    attr.get(function(d){
      done += 1;
      data[ attr.name ] = d;
      executeAfter(done, count, [ data, config ], cb);
    });
  });
};

// bufferData is triggered by the buffer_click listener in
// the buffer-browser-embed file, where it's passed a port
// to communicate with the extension and data sent from the
// background page.
var bufferData = function (port, postData) {

  if (window.top !== window) return;

  // Transform the data somewhat, and then create an overlay.
  // When it's done, fire buffer_done back to the extension
  var createOverlay = function (data, config) {
    if( data.embed ) {
      if( typeof data.embed === "object" ) {
        for( var i in data.embed ) {
          if( data.embed.hasOwnProperty(i) ) {
            data[i] = data.embed[i];
          }
        }
        if( data.embed.text && !data.embed.url ) {
          data.url = null;
        }
        data.embed = null;
      } else {
        data.text = data.embed;
        data.url = null;
        data.embed = null;
      }
    }
    bufferOverlay(data, config, function (overlaydata) {
      port.emit("buffer_done", overlaydata);
    });
  };

  // It all starts here.
  // createOverlay is the callback that should fire after getData has
  // gathered all the necessaries
  getData(postData, createOverlay);
};
<|endoftext|>


<commit_before>export class Settings {
  defaultTheme = 'default';
  activeTheme = 'default';
  gistProxy = 'http://gist-serve.jeroenvinke.nl/';
}
<commit_msg>no message<commit_after>export class Settings {
  defaultTheme = 'default';
  activeTheme = 'default';
  gistProxy = 'https://gist-serve.jeroenvinke.nl/';
}
<|endoftext|>


<commit_before>/*!
 * Advertisement data
 * Template file v0.7.0
 * © 2016-2017 Manhhailua
 * Zone: {{zoneId}}
 */
/** ****/ (function (modules) { // webpackBootstrap
/** ****/ 	// The module cache
  /** ****/ 	const installedModules = {};
/** ****/
/** ****/ 	// The require function
  /** ****/ 	function __webpack_require__(moduleId) {
/** ****/
/** ****/ 		// Check if module is in cache
    /** ****/ 		if (installedModules[moduleId])
  /** ****/ 			{
return installedModules[moduleId].exports; }
/** ****/
/** ****/ 		// Create a new module (and put it into the cache)
    /** ****/ 		const module = installedModules[moduleId] = {
    /** ****/ 			exports: {},
    /** ****/ 			id: moduleId,
    /** ****/ 			loaded: false,
  /** ****/ 		};
/** ****/
/** ****/ 		// Execute the module function
    /** ****/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/** ****/
/** ****/ 		// Flag the module as loaded
    /** ****/ 		module.loaded = true;
/** ****/
/** ****/ 		// Return the exports of the module
    /** ****/ 		return module.exports;
  /** ****/ 	}
/** ****/
/** ****/
/** ****/ 	// expose the modules object (__webpack_modules__)
  /** ****/ 	__webpack_require__.m = modules;
/** ****/
/** ****/ 	// expose the module cache
  /** ****/ 	__webpack_require__.c = installedModules;
/** ****/
/** ****/ 	// __webpack_public_path__
  /** ****/ 	__webpack_require__.p = '';
/** ****/
/** ****/ 	// Load entry module and return exports
  /** ****/ 	return __webpack_require__(0);
/** ****/ }([
/* 0 */
  /** */ function (module, exports) {

	/**
	 * Created by manhhailua on 1/4/17.
	 */

	let env = location.search.indexOf('corejs_env=dev') !== -1 ? '' : '.min';
	let script = document.createElement('script');

  script.id = 'arf-core-js';
  script.type = 'text/javascript';
  script.src = `//corejs.manhhailua.com/build/Arf${  env  }.js`;

	/**
	 * Async load core-js script
	 */
  if (!document.getElementById(script.id)) {
	  document.getElementsByTagName('body')[0].appendChild(script);
}

	// Init arfZonesQueue if not existed
  window.arfZonesQueue = window.arfZonesQueue || [];

	// Push current zone to arfZonesQueue
	// In production mode, webpack will force double quotes for string
  window.arfZonesQueue.push("{{zoneDataObject}}"); // eslint-disable-line quotes

/** *
/ },
/** ****/ ]));
// # sourceMappingURL=Template.js.map
<commit_msg>new bundle<commit_after>/*!
 * Advertisement data
 * Template file v0.7.0
 * © 2016-2017 Manhhailua
 * Zone: {{zoneId}}
 */
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;
/******/
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports) {

	'use strict';

	/**
	 * Created by manhhailua on 1/4/17.
	 */

	var env = location.search.indexOf('corejs_env=dev') !== -1 ? '' : '.min';
	var script = document.createElement('script');

	script.id = 'arf-core-js';
	script.type = 'text/javascript';
	script.src = '//corejs.manhhailua.com/build/Arf' + env + '.js';

	/**
	 * Async load core-js script
	 */
	if (!document.getElementById(script.id)) {
	  document.getElementsByTagName('body')[0].appendChild(script);
	}

	// Init arfZonesQueue if not existed
	window.arfZonesQueue = window.arfZonesQueue || [];

	// Push current zone to arfZonesQueue
	// In production mode, webpack will force double quotes for string
	window.arfZonesQueue.push("{{zoneDataObject}}"); // eslint-disable-line quotes

/***/ }
/******/ ]);
//# sourceMappingURL=Template.js.map<|endoftext|>


<commit_before>// Пустая клетка
var B = false;
// Клетка с фигурой
var Q = true;

// Тип данных: Board
// Пустая доска с размерностью n=4
var n4emptBrd = [B, B, B, B,
                 B, B, B, B,
                 B, B, B, B,
                 B, B, B, B];
// Решение для доски с размерностью 4
var n4solBrd = [B, Q, B, B,
                B, B, B, Q,
                Q, B, B, B,
                B, B, Q, B];

// Конвертирует позицию вида ряд:столбец
// в индекс плоского массива
// Все индексы начинаются с нуля.
// @param  {Integer[0...]} r Ряд
// @param  {Integer[0...]} c Столбец
// @param  {Natural}       n Размерность доски
// @return {Natural} Позиция
function rcToPos(r,c,n){
  return r*n+c;
};

// Тесты для функции rcToPos
(function(){
  console.log("Тест1:", n4solBrd[rcToPos(0,1,4)]==Q);
  console.log("Тест2:", n4solBrd[rcToPos(3,3,4)]==B);
})();

// Создает пустую доску n*n элементов
// @param {Natural} n Размерность доски
// @return {Board}
function createEmptyBoard(n){
  var b = [];
  for(var i=0;i<n*n;i++){ b.push(B); }
  return b;
};

// Тесты для функции createEmptyBoard
(function(){
  console.log("Тест3:", createEmptyBoard(4).length==16);
})();

// Решает проблемы n-ферзей
// @param {Board} b Доска
// @return {Board | false}:
//          Если задача имеет решение тогда доску
//          Если решения нет тогда false

// function solve(b){return false}; // заглушка

function solve(b){
  // Ищет решение для данной доски
  // @param {Board} b
  // @return {Board | false}
  function solveBrd(b){
    if(isSolved(b)){
      return b;
    }else{
      return solveChilds(nextBoards(b));
    }
  };

  // Ищет решения для списка потомков
  // @param {arrayof: Board} lob
  function solveChilds(lob){

  };

  return solveBrd(b);
};

(function(){
  console.log("Тест4:", solve(createEmptyBoard(1))==[Q]);
  console.log("Тест5:", solve(createEmptyBoard(2))==false);
  console.log("Тест6:", solve(createEmptyBoard(4))==n4solBrd);
})();

// Является ли данная доска решением?
// @param  {Board} b
// @return {Boolean}
// !!!
function isSolved(b){ return false; } // Заглушка

// Создает массив потомков данной доски
// Все несостоятельные доски вырезаются
// @param  {Board} b
// @return {arrayof: Board}
// !!!
function nextBoards(b){ return []; } // Заглушка

<commit_msg>Функция solveChilds<commit_after>// Пустая клетка
var B = false;
// Клетка с фигурой
var Q = true;

// Тип данных: Board
// Пустая доска с размерностью n=4
var n4emptBrd = [B, B, B, B,
                 B, B, B, B,
                 B, B, B, B,
                 B, B, B, B];
// Решение для доски с размерностью 4
var n4solBrd = [B, Q, B, B,
                B, B, B, Q,
                Q, B, B, B,
                B, B, Q, B];

// Конвертирует позицию вида ряд:столбец
// в индекс плоского массива
// Все индексы начинаются с нуля.
// @param  {Integer[0...]} r Ряд
// @param  {Integer[0...]} c Столбец
// @param  {Natural}       n Размерность доски
// @return {Natural} Позиция
function rcToPos(r,c,n){
  return r*n+c;
};

// Тесты для функции rcToPos
(function(){
  console.log("Тест1:", n4solBrd[rcToPos(0,1,4)]==Q);
  console.log("Тест2:", n4solBrd[rcToPos(3,3,4)]==B);
})();

// Создает пустую доску n*n элементов
// @param {Natural} n Размерность доски
// @return {Board}
function createEmptyBoard(n){
  var b = [];
  for(var i=0;i<n*n;i++){ b.push(B); }
  return b;
};

// Тесты для функции createEmptyBoard
(function(){
  console.log("Тест3:", createEmptyBoard(4).length==16);
})();

// Решает проблемы n-ферзей
// @param {Board} b Доска
// @return {Board | false}:
//          Если задача имеет решение тогда доску
//          Если решения нет тогда false

// function solve(b){return false}; // заглушка

function solve(b){
  // Ищет решение для данной доски
  // @param {Board} b
  // @return {Board | false}
  function solveBrd(b){
    if(isSolved(b)){
      return b;
    }else{
      return solveChilds(nextBoards(b));
    }
  };

  // Ищет решения для списка потомков
  // @param {arrayof: Board} lob
  // @return {Board | false}
  function solveChilds(lob){
    if(lob.length==0){
      return false; // базовый случай
    }else{
      // рекурсивный случай
      var first = lob[0];
      var rest  = lob.slice(1,lob.length);
      var tryBrd = solveBrd(first);
      if(tryBrd!=false){
        return tryBrd;
      }else{
        return solveChilds(rest);
      }
    }
  };

  return solveBrd(b);
};

(function(){
  console.log("Тест4:", solve(createEmptyBoard(1))==[Q]);
  console.log("Тест5:", solve(createEmptyBoard(2))==false);
  console.log("Тест6:", solve(createEmptyBoard(4))==n4solBrd);
})();

// Является ли данная доска решением?
// @param  {Board} b
// @return {Boolean}
// !!!
function isSolved(b){ return false; } // Заглушка

// Создает массив потомков данной доски
// Все несостоятельные доски вырезаются
// @param  {Board} b
// @return {arrayof: Board}
// !!!
function nextBoards(b){ return []; } // Заглушка

<|endoftext|>