<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';

var instance = null;
const log = require("./logger.js")("PolicyManager");
const sysManager = require('./SysManager.js');
const rclient = require('../util/redis_manager.js').getRedisClient()
const pclient = require('../util/redis_manager.js').getPublishClient()
const Message = require('./Message.js');
const fc = require('../net2/config.js');

const iptable = require('./Iptables.js');
const ip6table = require('./Ip6tables.js');

const Block = require('../control/Block.js');

const VpnManager = require('../vpn/VpnManager.js');

const extensionManager = require('../sensor/ExtensionManager.js')

const UPNP = require('../extension/upnp/upnp');
const upnp = new UPNP();

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

let externalAccessFlag = false;

const delay = require('../util/util.js').delay;

const localPort = 8833;
const externalPort = 8833;
const UPNP_INTERVAL = 3600;  // re-send upnp port request every hour

const sem = require('../sensor/SensorEventManager.js').getInstance();
const platformLoader = require('../platform/PlatformLoader.js');
const platform = platformLoader.getPlatform();

const { Rule } = require('../net2/Iptables.js');

const util = require('util')

const { exec } = require('child-process-promise')

let iptablesReady = false

module.exports = class {
  constructor() {
    if (instance == null) {
      instance = this;
    }
    return instance;
  }

  // this should flush ip6tables as well
  async flush(config) {
    iptablesReady = false

    if (require('./UpgradeManager.js').isUpgrading() == true) {
      return;
    }

    await ip6table.prepare();
    await iptable.prepare();
    await ip6table.flush()
    await iptable.flush()

    // In case diag service is running, immediate adds redirection back to prevent pairing failure
    sem.emitEvent({
      type: 'DiagRedirectionRenew',
      toProcess: 'FireKick',
      message: 'Iptables flushed by FireMain'
    })

    // ======= default iptables =======
    const secondarySubnet = sysManager.mySubnet2();
    if (platform.getDHCPCapacity() && secondarySubnet) {
      const overlayMasquerade =
        new Rule('nat').chn('FW_POSTROUTING').mth(secondarySubnet, null, 'src').jmp('MASQUERADE');
      await exec(overlayMasquerade.toCmd('-A'));
    }
    const icmpv6Redirect =
      new Rule().fam(6).chn('OUTPUT').pro('icmpv6').pam('--icmpv6-type redirect').jmp('DROP');
    await exec(icmpv6Redirect.toCmd('-D'));
    await exec(icmpv6Redirect.toCmd('-I'));

    // Setup iptables so that it's ready for blocking
    await Block.setupBlockChain();

    // setup global blocking redis match rule
    await dnsmasq.createGlobalRedisMatchRule();

    // setup active protect category mapping file
    await dnsmasq.createCategoryMappingFile("default_c");

    iptablesReady = true

    sem.emitEvent({
      type: 'IPTABLES_READY'
    });
  }

  async upstreamDns(policy) {

    log.info("PolicyManager:UpstreamDns:Dnsmasq", policy);
    const ips = policy.ips;
    const state = policy.state;
    const featureName = "upstream_dns";
    if (state === true) {
      await fc.enableDynamicFeature(featureName);
      dnsmasq.setDefaultNameServers("00-upstream", ips);
      await dnsmasq.updateResolvConf();
    } else {
      await fc.disableDynamicFeature(featureName);
      dnsmasq.unsetDefaultNameServers("00-upstream"); // reset dns name servers to null no matter whether iptables dns change is failed or successful
      await dnsmasq.updateResolvConf();
    }
  }

  async getUpstreamDns() {
    log.info("PolicyManager:UpstreamDns:getUpstreamDns");
    let value = await rclient.hgetAsync('sys:features', 'upstream_dns');

    let resp = {};
    if (value === "1") { // enabled
      resp.enabled = true;
      let ips = await dnsmasq.getCurrentNameServerList();
      resp.ip = ips[0];
    } else {
      resp.enabled = false;
    }
    return resp;
  }

  async vpnClient(target, policy) {
    if (!target)
      return;
    switch (target.constructor.name) {
      case "HostManager": {
        const result = await target.vpnClient(policy); // result optionally contains value of state and running
        const latestPolicy = target.getPolicyFast() || {}; // in case latest policy has changed before the vpnClient function returns
        const updatedPolicy = Object.assign({}, latestPolicy.vpnClient || policy, result); // this may trigger an extra system policy apply but the result should be idempotent
        target.setPolicy("vpnClient", updatedPolicy);
        break;
      }
      default: {
        await target.vpnClient(policy);
        break;
      }
    }

  }

  async ipAllocation(host, policy) {
    if (!host || host.constructor.name !== 'Host') {
      log.error("ipAllocation only supports per device policy", host && host.constructor.name);
      return;
    }
    await host.ipAllocation(policy);
  }

  async enhancedSpoof(host, state) {
    if (host.constructor.name !== 'HostManager') {
      log.error("enhancedSpoof doesn't support per device policy", host);
      return;
    }
    host.enhancedSpoof(state);
  }

  async vpn(host, config, policies) {
    if(host.constructor.name !== 'HostManager') {
      log.error("vpn doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    let vpnManager = new VpnManager();
    let conf = await vpnManager.configure(config);
    if (conf == null) {
      log.error("PolicyManager:VPN", "Failed to configure vpn");
      return;
    }

    if (policies.vpnAvaliable == null || policies.vpnAvaliable == false) {
      conf = await vpnManager.stop();
      log.error("PolicyManager:VPN", "VPN Not avaliable");
      const updatedConfig = Object.assign({}, config, conf);
      await host.setPolicyAsync("vpn", updatedConfig);
      return;
    }
    if (config.state == true) {
      conf = await vpnManager.start();
    } else {
      conf = await vpnManager.stop();
    }
    // vpnManager.start() will return latest status of VPN server, which needs to be updated and re-enforced in system policy
    const updatedConfig = Object.assign({}, config, conf);
    await host.setPolicyAsync("vpn", updatedConfig)
    await host.setPolicyAsync("vpnPortmapped", updatedConfig.portmapped);
  }

  async whitelist(host, config) {
  }

  shadowsocks(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("shadowsocks doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    let shadowsocks = require('../extension/shadowsocks/shadowsocks.js');
    let ss = new shadowsocks('info');

    // ss.refreshConfig();
    if (!ss.configExists()) {
      log.info("Generating shadowsocks config");
      ss.refreshConfig();
    }

    if (config.state == true) {
      ss.start((err) => {
        if (err == null) {
          log.info("Shadowsocks service is started successfully");
        } else {
          log.error("Failed to start shadowsocks: " + err);
        }
      })
    } else {
      ss.stop((err) => {
        if (err == null) {
          log.info("Shadowsocks service is stopped successfully");
        } else {
          log.error("Failed to stop shadowsocks: " + err);
        }
      })
    }
  }

  dnsmasq(host, config) {
    if(host.constructor.name !== 'HostManager') {
      // per-device or per-network dnsmasq policy
      host._dnsmasq(config);
      return;
    }
    let needUpdate = false;
    let needRestart = false;
    if (config.secondaryDnsServers && Array.isArray(config.secondaryDnsServers)) {
      dnsmasq.setInterfaceNameServers("secondary", config.secondaryDnsServers);
      needUpdate = true;
    }
    if (config.alternativeDnsServers && Array.isArray(config.alternativeDnsServers)) {
      dnsmasq.setInterfaceNameServers("alternative", config.alternativeDnsServers);
      needUpdate = true;
    }
    if (config.secondaryDhcpRange) {
      dnsmasq.setDhcpRange("secondary", config.secondaryDhcpRange.begin, config.secondaryDhcpRange.end);
      needRestart = true;
    }
    if (config.alternativeDhcpRange) {
      dnsmasq.setDhcpRange("alternative", config.alternativeDhcpRange.begin, config.alternativeDhcpRange.end);
      needRestart = true;
    }
    if (needUpdate)
      dnsmasq.updateResolvConf();
    if (needRestart)
      dnsmasq.start(true);
  }

  addAPIPortMapping(time) {
    time = time || 1;
    setTimeout(() => {
      if (!externalAccessFlag) {
        log.info("Cancel addAPIPortMapping scheduler since externalAccessFlag is now off");
        return; // exit if the flag is still off
      }

      upnp.addPortMapping("tcp", localPort, externalPort, "Firewalla API");
      this.addAPIPortMapping(UPNP_INTERVAL * 1000); // add port every hour
    }, time)
  }

  removeAPIPortMapping(time) {
    time = time || 1;

    setTimeout(() => {
      if (externalAccessFlag) {
        log.info("Cancel removeAPIPortMapping scheduler since externalAccessFlag is now on");
        return; // exit if the flag is still on
      }

      upnp.removePortMapping("tcp", localPort, externalPort);
      this.removeAPIPortMapping(UPNP_INTERVAL * 1000); // remove port every hour
    }, time)

  }

  externalAccess(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("externalAccess doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    if (config.state == true) {
      externalAccessFlag = true;
      this.addAPIPortMapping();
    } else {
      externalAccessFlag = false;
      this.removeAPIPortMapping();
    }
  }

  async apiInterface(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("apiInterface doesn't support per device policy", host);
      return;
    }

    await pclient.publishAsync(Message.MSG_SYS_API_INTERFACE_CHANGED, JSON.stringify(config))
  }

  tags(target, config) {
    if (!target)
      return;
    if (target.constructor.name === 'HostManager') {
      log.error("tags doesn't support system policy");
      return;
    }
    target.tags(config);
  }

  execute(target, ip, policy, callback) {
    if (target.oper == null) {
      target.oper = {};
    }

    if (policy == null || Object.keys(policy).length == 0) {
      log.debug("PolicyManager:Execute:NoPolicy", ip, policy);
      target.spoof(true);
      target.oper['monitor'] = true;
      if (ip === "0.0.0.0" && target.constructor.name === "HostManager") {
        target.qos(false);
        target.oper['qos'] = false;
      }
      if (callback)
        callback(null, null);
      return;
    }
    log.debug("PolicyManager:Execute:", ip, policy);

    if (ip === '0.0.0.0' && target.constructor.name === "HostManager" && !policy.hasOwnProperty('qos')) {
      policy['qos'] = false;
    }

    for (let p in policy) {
      if (target.oper[p] !== undefined && JSON.stringify(target.oper[p]) === JSON.stringify(policy[p])) {
        log.debug("PolicyManager:AlreadyApplied", p, target.oper[p]);
        if (p === "monitor") {
          target.spoof(policy[p]);
        }
        continue;
      }
      // If any extension support this 'applyPolicy' hook, call it
      if (extensionManager.hasExtension(p)) {
        let hook = extensionManager.getHook(p, "applyPolicy")
        if (hook) {
          try {
            hook(target, ip, policy[p])
          } catch (err) {
            log.error(`Failed to call applyPolicy hook on target ${ip} policy ${p}, err: ${err}`)
          }
        }
      }
      if (p === "domains_keep_local") {
        (async () => {
          try {
            await dnsmasq.keepDomainsLocal(p, policy[p])
          } catch (err) {
            log.error("Error when set local domain", err);
          }
        })();
      }
      if (p === "upstreamDns") {
        (async () => {
          try {
            await this.upstreamDns(policy[p]);
          } catch (err) {
            log.error("Error when set upstream dns", err);
          }
        })();
      } else if (p === "monitor") {
        target.spoof(policy[p]);
      } else if (p === "qos") {
        target.qos(policy[p]);
      } else if (p === "acl") {
        target.acl(policy[p]);
      } else if (p === "aclTimer") {
        target.aclTimer(policy[p]);
      } else if (p === "vpnClient") {
        this.vpnClient(target, policy[p]);
      } else if (p === "vpn") {
        this.vpn(target, policy[p], policy);
      } else if (p === "shadowsocks") {
        this.shadowsocks(target, policy[p]);
      } else if (p === "whitelist") {
        this.whitelist(target, policy[p]);
      } else if (p === "shield") {
        target.shield(policy[p]);
      } else if (p === "enhancedSpoof") {
        this.enhancedSpoof(target, policy[p]);
      } else if (p === "externalAccess") {
        this.externalAccess(target, policy[p]);
      } else if (p === "apiInterface") {
        this.apiInterface(target, policy[p]);
      } else if (p === "ipAllocation") {
        this.ipAllocation(target, policy[p]);
      } else if (p === "dnsmasq") {
        // do nothing here, will handle dnsmasq at the end
      } else if (p === "tags") {
        this.tags(target, policy[p]);
      }

      if (p !== "dnsmasq") {
        target.oper[p] = policy[p];
      }

    }

    // put dnsmasq logic at the end, as it is foundation feature

    if (policy["dnsmasq"]) {
      if (target.oper["dnsmasq"] != null &&
        JSON.stringify(target.oper["dnsmasq"]) === JSON.stringify(policy["dnsmasq"])) {
        // do nothing
      } else {
        this.dnsmasq(target, policy["dnsmasq"]);
        target.oper["dnsmasq"] = policy["dnsmasq"];
      }
    }

    if (policy['monitor'] == null) {
      log.debug("PolicyManager:ApplyingMonitor", ip);
      target.spoof(true);
      log.debug("PolicyManager:ApplyingMonitorDone", ip);
      target.oper['monitor'] = true;
    }

    if (callback)
      callback(null, null);
  }

  executeAsync(target, ip, policy) {
    return util.promisify(this.execute).bind(this)(target, ip, policy)
  }
}
<commit_msg>fixed bug that policy data may be altered by applyPolicy function<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';

var instance = null;
const log = require("./logger.js")("PolicyManager");
const sysManager = require('./SysManager.js');
const rclient = require('../util/redis_manager.js').getRedisClient()
const pclient = require('../util/redis_manager.js').getPublishClient()
const Message = require('./Message.js');
const fc = require('../net2/config.js');

const iptable = require('./Iptables.js');
const ip6table = require('./Ip6tables.js');

const Block = require('../control/Block.js');

const VpnManager = require('../vpn/VpnManager.js');

const extensionManager = require('../sensor/ExtensionManager.js')

const UPNP = require('../extension/upnp/upnp');
const upnp = new UPNP();

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

let externalAccessFlag = false;

const delay = require('../util/util.js').delay;

const localPort = 8833;
const externalPort = 8833;
const UPNP_INTERVAL = 3600;  // re-send upnp port request every hour

const sem = require('../sensor/SensorEventManager.js').getInstance();
const platformLoader = require('../platform/PlatformLoader.js');
const platform = platformLoader.getPlatform();

const { Rule } = require('../net2/Iptables.js');

const util = require('util')

const { exec } = require('child-process-promise')

let iptablesReady = false

module.exports = class {
  constructor() {
    if (instance == null) {
      instance = this;
    }
    return instance;
  }

  // this should flush ip6tables as well
  async flush(config) {
    iptablesReady = false

    if (require('./UpgradeManager.js').isUpgrading() == true) {
      return;
    }

    await ip6table.prepare();
    await iptable.prepare();
    await ip6table.flush()
    await iptable.flush()

    // In case diag service is running, immediate adds redirection back to prevent pairing failure
    sem.emitEvent({
      type: 'DiagRedirectionRenew',
      toProcess: 'FireKick',
      message: 'Iptables flushed by FireMain'
    })

    // ======= default iptables =======
    const secondarySubnet = sysManager.mySubnet2();
    if (platform.getDHCPCapacity() && secondarySubnet) {
      const overlayMasquerade =
        new Rule('nat').chn('FW_POSTROUTING').mth(secondarySubnet, null, 'src').jmp('MASQUERADE');
      await exec(overlayMasquerade.toCmd('-A'));
    }
    const icmpv6Redirect =
      new Rule().fam(6).chn('OUTPUT').pro('icmpv6').pam('--icmpv6-type redirect').jmp('DROP');
    await exec(icmpv6Redirect.toCmd('-D'));
    await exec(icmpv6Redirect.toCmd('-I'));

    // Setup iptables so that it's ready for blocking
    await Block.setupBlockChain();

    // setup global blocking redis match rule
    await dnsmasq.createGlobalRedisMatchRule();

    // setup active protect category mapping file
    await dnsmasq.createCategoryMappingFile("default_c");

    iptablesReady = true

    sem.emitEvent({
      type: 'IPTABLES_READY'
    });
  }

  async upstreamDns(policy) {

    log.info("PolicyManager:UpstreamDns:Dnsmasq", policy);
    const ips = policy.ips;
    const state = policy.state;
    const featureName = "upstream_dns";
    if (state === true) {
      await fc.enableDynamicFeature(featureName);
      dnsmasq.setDefaultNameServers("00-upstream", ips);
      await dnsmasq.updateResolvConf();
    } else {
      await fc.disableDynamicFeature(featureName);
      dnsmasq.unsetDefaultNameServers("00-upstream"); // reset dns name servers to null no matter whether iptables dns change is failed or successful
      await dnsmasq.updateResolvConf();
    }
  }

  async getUpstreamDns() {
    log.info("PolicyManager:UpstreamDns:getUpstreamDns");
    let value = await rclient.hgetAsync('sys:features', 'upstream_dns');

    let resp = {};
    if (value === "1") { // enabled
      resp.enabled = true;
      let ips = await dnsmasq.getCurrentNameServerList();
      resp.ip = ips[0];
    } else {
      resp.enabled = false;
    }
    return resp;
  }

  async vpnClient(target, policy) {
    if (!target)
      return;
    switch (target.constructor.name) {
      case "HostManager": {
        const result = await target.vpnClient(policy); // result optionally contains value of state and running
        const latestPolicy = target.getPolicyFast() || {}; // in case latest policy has changed before the vpnClient function returns
        const updatedPolicy = Object.assign({}, latestPolicy.vpnClient || policy, result); // this may trigger an extra system policy apply but the result should be idempotent
        target.setPolicy("vpnClient", updatedPolicy);
        break;
      }
      default: {
        await target.vpnClient(policy);
        break;
      }
    }

  }

  async ipAllocation(host, policy) {
    if (!host || host.constructor.name !== 'Host') {
      log.error("ipAllocation only supports per device policy", host && host.constructor.name);
      return;
    }
    await host.ipAllocation(policy);
  }

  async enhancedSpoof(host, state) {
    if (host.constructor.name !== 'HostManager') {
      log.error("enhancedSpoof doesn't support per device policy", host);
      return;
    }
    host.enhancedSpoof(state);
  }

  async vpn(host, config, policies) {
    if(host.constructor.name !== 'HostManager') {
      log.error("vpn doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    let vpnManager = new VpnManager();
    let conf = await vpnManager.configure(config);
    if (conf == null) {
      log.error("PolicyManager:VPN", "Failed to configure vpn");
      return;
    }

    if (policies.vpnAvaliable == null || policies.vpnAvaliable == false) {
      conf = await vpnManager.stop();
      log.error("PolicyManager:VPN", "VPN Not avaliable");
      const updatedConfig = Object.assign({}, config, conf);
      await host.setPolicyAsync("vpn", updatedConfig);
      return;
    }
    if (config.state == true) {
      conf = await vpnManager.start();
    } else {
      conf = await vpnManager.stop();
    }
    // vpnManager.start() will return latest status of VPN server, which needs to be updated and re-enforced in system policy
    const updatedConfig = Object.assign({}, config, conf);
    await host.setPolicyAsync("vpn", updatedConfig)
    await host.setPolicyAsync("vpnPortmapped", updatedConfig.portmapped);
  }

  async whitelist(host, config) {
  }

  shadowsocks(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("shadowsocks doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    let shadowsocks = require('../extension/shadowsocks/shadowsocks.js');
    let ss = new shadowsocks('info');

    // ss.refreshConfig();
    if (!ss.configExists()) {
      log.info("Generating shadowsocks config");
      ss.refreshConfig();
    }

    if (config.state == true) {
      ss.start((err) => {
        if (err == null) {
          log.info("Shadowsocks service is started successfully");
        } else {
          log.error("Failed to start shadowsocks: " + err);
        }
      })
    } else {
      ss.stop((err) => {
        if (err == null) {
          log.info("Shadowsocks service is stopped successfully");
        } else {
          log.error("Failed to stop shadowsocks: " + err);
        }
      })
    }
  }

  dnsmasq(host, config) {
    if(host.constructor.name !== 'HostManager') {
      // per-device or per-network dnsmasq policy
      host._dnsmasq(config);
      return;
    }
    let needUpdate = false;
    let needRestart = false;
    if (config.secondaryDnsServers && Array.isArray(config.secondaryDnsServers)) {
      dnsmasq.setInterfaceNameServers("secondary", config.secondaryDnsServers);
      needUpdate = true;
    }
    if (config.alternativeDnsServers && Array.isArray(config.alternativeDnsServers)) {
      dnsmasq.setInterfaceNameServers("alternative", config.alternativeDnsServers);
      needUpdate = true;
    }
    if (config.secondaryDhcpRange) {
      dnsmasq.setDhcpRange("secondary", config.secondaryDhcpRange.begin, config.secondaryDhcpRange.end);
      needRestart = true;
    }
    if (config.alternativeDhcpRange) {
      dnsmasq.setDhcpRange("alternative", config.alternativeDhcpRange.begin, config.alternativeDhcpRange.end);
      needRestart = true;
    }
    if (needUpdate)
      dnsmasq.updateResolvConf();
    if (needRestart)
      dnsmasq.start(true);
  }

  addAPIPortMapping(time) {
    time = time || 1;
    setTimeout(() => {
      if (!externalAccessFlag) {
        log.info("Cancel addAPIPortMapping scheduler since externalAccessFlag is now off");
        return; // exit if the flag is still off
      }

      upnp.addPortMapping("tcp", localPort, externalPort, "Firewalla API");
      this.addAPIPortMapping(UPNP_INTERVAL * 1000); // add port every hour
    }, time)
  }

  removeAPIPortMapping(time) {
    time = time || 1;

    setTimeout(() => {
      if (externalAccessFlag) {
        log.info("Cancel removeAPIPortMapping scheduler since externalAccessFlag is now on");
        return; // exit if the flag is still on
      }

      upnp.removePortMapping("tcp", localPort, externalPort);
      this.removeAPIPortMapping(UPNP_INTERVAL * 1000); // remove port every hour
    }, time)

  }

  externalAccess(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("externalAccess doesn't support per device policy", host);
      return; // doesn't support per-device policy
    }

    if (config.state == true) {
      externalAccessFlag = true;
      this.addAPIPortMapping();
    } else {
      externalAccessFlag = false;
      this.removeAPIPortMapping();
    }
  }

  async apiInterface(host, config) {
    if(host.constructor.name !== 'HostManager') {
      log.error("apiInterface doesn't support per device policy", host);
      return;
    }

    await pclient.publishAsync(Message.MSG_SYS_API_INTERFACE_CHANGED, JSON.stringify(config))
  }

  tags(target, config) {
    if (!target)
      return;
    if (target.constructor.name === 'HostManager') {
      log.error("tags doesn't support system policy");
      return;
    }
    target.tags(config);
  }

  execute(target, ip, policy, callback) {
    if (target.oper == null) {
      target.oper = {};
    }

    if (policy == null || Object.keys(policy).length == 0) {
      log.debug("PolicyManager:Execute:NoPolicy", ip, policy);
      target.spoof(true);
      target.oper['monitor'] = true;
      if (ip === "0.0.0.0" && target.constructor.name === "HostManager") {
        target.qos(false);
        target.oper['qos'] = false;
      }
      if (callback)
        callback(null, null);
      return;
    }
    log.debug("PolicyManager:Execute:", ip, policy);

    if (ip === '0.0.0.0' && target.constructor.name === "HostManager" && !policy.hasOwnProperty('qos')) {
      policy['qos'] = false;
    }

    for (let p in policy) {
      // keep a clone of the policy object to make sure the original policy data is not changed
      // the original data will be used for comparison to know if configured policy is updated,
      // if not updated, the applyPolicy below will not be changed

      const policyDataClone = JSON.parse(JSON.stringify(policy[p]));

      if (target.oper[p] !== undefined && JSON.stringify(target.oper[p]) === JSON.stringify(policy[p])) {
        log.debug("PolicyManager:AlreadyApplied", p, target.oper[p]);
        if (p === "monitor") {
          target.spoof(policy[p]);
        }
        continue;
      }
      // If any extension support this 'applyPolicy' hook, call it
      if (extensionManager.hasExtension(p)) {
        let hook = extensionManager.getHook(p, "applyPolicy")
        if (hook) {
          try {
            hook(target, ip, policyDataClone)
          } catch (err) {
            log.error(`Failed to call applyPolicy hook on target ${ip} policy ${p}, err: ${err}`)
          }
        }
      }
      if (p === "domains_keep_local") {
        (async () => {
          try {
            await dnsmasq.keepDomainsLocal(p, policyDataClone)
          } catch (err) {
            log.error("Error when set local domain", err);
          }
        })();
      }
      if (p === "upstreamDns") {
        (async () => {
          try {
            await this.upstreamDns(policyDataClone);
          } catch (err) {
            log.error("Error when set upstream dns", err);
          }
        })();
      } else if (p === "monitor") {
        target.spoof(policyDataClone);
      } else if (p === "qos") {
        target.qos(policyDataClone);
      } else if (p === "acl") {
        target.acl(policyDataClone);
      } else if (p === "aclTimer") {
        target.aclTimer(policyDataClone);
      } else if (p === "vpnClient") {
        this.vpnClient(target, policyDataClone);
      } else if (p === "vpn") {
        this.vpn(target, policyDataClone, policy);
      } else if (p === "shadowsocks") {
        this.shadowsocks(target, policyDataClone);
      } else if (p === "whitelist") {
        this.whitelist(target, policyDataClone);
      } else if (p === "shield") {
        target.shield(policyDataClone);
      } else if (p === "enhancedSpoof") {
        this.enhancedSpoof(target, policyDataClone);
      } else if (p === "externalAccess") {
        this.externalAccess(target, policyDataClone);
      } else if (p === "apiInterface") {
        this.apiInterface(target, policyDataClone);
      } else if (p === "ipAllocation") {
        this.ipAllocation(target, policyDataClone);
      } else if (p === "dnsmasq") {
        // do nothing here, will handle dnsmasq at the end
      } else if (p === "tags") {
        this.tags(target, policyDataClone);
      }

      if (p !== "dnsmasq") {
        target.oper[p] = policy[p]; // use original policy data instead of the possible-changed clone
      }

    }

    // put dnsmasq logic at the end, as it is foundation feature

    if (policy["dnsmasq"]) {
      if (target.oper["dnsmasq"] != null &&
        JSON.stringify(target.oper["dnsmasq"]) === JSON.stringify(policy["dnsmasq"])) {
        // do nothing
      } else {
        this.dnsmasq(target, policy["dnsmasq"]);
        target.oper["dnsmasq"] = policy["dnsmasq"];
      }
    }

    if (policy['monitor'] == null) {
      log.debug("PolicyManager:ApplyingMonitor", ip);
      target.spoof(true);
      log.debug("PolicyManager:ApplyingMonitorDone", ip);
      target.oper['monitor'] = true;
    }

    if (callback)
      callback(null, null);
  }

  executeAsync(target, ip, policy) {
    return util.promisify(this.execute).bind(this)(target, ip, policy)
  }
}
<|endoftext|>


<commit_before>import gravity from '../../lib/loaders/gravity';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { artworkFields } from '../artwork/index';

export default mutationWithClientMutationId({
  name: 'SaveArtwork',
  decription: 'Save (or remove) an artwork to (from) a users default collection.',
  inputFields: {
    artwork_id: {
      type: GraphQLString,
    },
    remove: {
      type: GraphQLBoolean,
    },
  },
  outputFields: artworkFields(),
  mutateAndGetPayload: ({
    artwork_id,
    remove,
  }, request, { rootValue: { accessToken, userID } }) => {
    if (!accessToken) return null;
    const saveMethod = remove ? 'DELETE' : 'POST';
    return gravity.with(accessToken, {
      method: saveMethod,
    })(`/collection/saved-artwork/artwork/${artwork_id}`, { user_id: userID });
  },
});
<commit_msg>Actually return artwork on save mutation<commit_after>import gravity from '../../lib/loaders/gravity';
import { GraphQLString, GraphQLBoolean } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { artworkFields } from '../artwork/index';

export default mutationWithClientMutationId({
  name: 'SaveArtwork',
  decription: 'Save (or remove) an artwork to (from) a users default collection.',
  inputFields: {
    artwork_id: {
      type: GraphQLString,
    },
    remove: {
      type: GraphQLBoolean,
    },
  },
  outputFields: artworkFields(),
  mutateAndGetPayload: ({
    artwork_id,
    remove,
  }, request, { rootValue: { accessToken, userID } }) => {
    if (!accessToken) return null;
    const saveMethod = remove ? 'DELETE' : 'POST';
    return gravity.with(accessToken, {
      method: saveMethod,
    })(`/collection/saved-artwork/artwork/${artwork_id}`, {
      user_id: userID,
    }).then(() => gravity(`artwork/${artwork_id}`));
  },
});
<|endoftext|>


<commit_before>define(
  [
    'underscore',
    'collections/MaintenanceMessageCollection',
    'dispatchers/Dispatcher',
    'stores/Store'
  ],
  function (_, MaintenanceMessageCollection, Dispatcher, Store) {

    var _messages = null;
    var _isFetching = false;

    var fetchMessages = function () {
      if(!_isFetching) {
        _isFetching = true;
        var messages = new MaintenanceMessageCollection();
        messages.fetch().done(function () {
          _isFetching = false;
          _messages = messages;
          MaintenanceMessageStore.emitChange();
        });
      }
    };

    var MaintenanceMessageStore = {

      getAll: function () {
        if(!_messages) {
          fetchMessages();
        } else {
          return _messages;
        }
      },

      isProviderInMaintenance: function(providerId){
        return _messages.where({provider_id: providerId}).length > 0;
      }

    };

    Dispatcher.register(function (payload) {
      var action = payload.action;

      switch (action.actionType) {

        default:
           return true;
      }

      MaintenanceMessageStore.emitChange();

      return true;
    });

    _.extend(MaintenanceMessageStore, Store);

    return MaintenanceMessageStore;
  });
<commit_msg>only disable provider if login is disabled<commit_after>define(
  [
    'underscore',
    'collections/MaintenanceMessageCollection',
    'dispatchers/Dispatcher',
    'stores/Store'
  ],
  function (_, MaintenanceMessageCollection, Dispatcher, Store) {

    var _messages = null;
    var _isFetching = false;

    var fetchMessages = function () {
      if(!_isFetching) {
        _isFetching = true;
        var messages = new MaintenanceMessageCollection();
        messages.fetch().done(function () {
          _isFetching = false;
          _messages = messages;
          MaintenanceMessageStore.emitChange();
        });
      }
    };

    var MaintenanceMessageStore = {

      getAll: function () {
        if(!_messages) {
          fetchMessages();
        } else {
          return _messages;
        }
      },

      isProviderInMaintenance: function(providerId){
        var providerMessages = _messages.where({provider_id: providerId}),
            isInMaintenance = false;

        providerMessages.forEach(function(message){
          if(message.get('disable_login') === true) {
            isInMaintenance = true;
          }
        });

        return isInMaintenance;
      }

    };

    Dispatcher.register(function (payload) {
      var action = payload.action;

      switch (action.actionType) {

        default:
           return true;
      }

      MaintenanceMessageStore.emitChange();

      return true;
    });

    _.extend(MaintenanceMessageStore, Store);

    return MaintenanceMessageStore;
  });
<|endoftext|>


<commit_before>// VVVV.js -- Visual Web Client Programming
// (c) 2011 Matthias Zauner
// VVVV.js is freely distributable under the MIT license.
// Additional authors of sub components are mentioned at the specific code locations.
// This component was developed is (c) 2014 Lukas Winter, distributed under the MIT license.

VVVV.PinTypes.WebAudio = {
  typeName: "WebAudio",
  reset_on_disconnect: true,
  defaultValue: function() {
    return "Unconnected Audio";
  },
  connectionChangedHandlers: {
    'webaudio': function() {
      if(this.direction == PinDirection.Output)
      {
        console.log("Connection changed!", this);
        this.audioConnectionChanged = true;
      }
    }
  }
}

var audioContext = null;

function WebAudioNode(id, name, graph) {
  if(graph) //constructing actual node
  {
    this.constructor(id, name, graph);
    if(!audioContext)
    {
      audioContext = new AudioContext();
    }
    this.initialize = function()
    {
      this.createAPINode();
      this.createAudioPins();
      this.createParamPins();
    }
    this.audioInputPins = [];
    this.audioOutputPins = [];
    this.paramPins = [];
  }
  else //constructing prototype
  {
    this.createAPINode = function(arg)
    {
      //this is just for debugging purposes with firefox's web audio visualiser
      if(id == 'Analyser')
        this.apiNode = audioContext.createAnalyser(arg);
      else if(id == 'MediaElementSource')
        this.apiNode = audioContext.createMediaElementSource(arg);
      else if(id == 'Oscillator')
        this.apiNode = audioContext.createOscillator(arg);
      else if(id == 'Delay')
        this.apiNode = audioContext.createDelay(arg);
      else if(id == 'Gain')
        this.apiNode = audioContext.createGain(arg);
      else if(id == 'DynamicsCompressor')
        this.apiNode = audioContext.createDynamicsCompressor(arg);
      else //this is the normal code
        this.apiNode = audioContext['create'+id].apply(audioContext, arguments);
    }
    this.auto_evaluate = false;
  }
}
WebAudioNode.prototype = new VVVV.Core.Node();
WebAudioNode.prototype.createAudioPins = function()
{
  for(var i = 0; i < this.apiNode.numberOfInputs; i++)
  {
    this.audioInputPins.push(this.addInputPin('Input '+(i+1), [], VVVV.PinTypes.WebAudio));
  }
  for(var i = 0; i < this.apiNode.numberOfOutputs; i++)
  {
    this.audioOutputPins.push(this.addOutputPin('Output '+(i+1), [this.apiNode], VVVV.PinTypes.WebAudio));
  }
}
WebAudioNode.prototype.createParamPins = function()
{
  for(var key in this.apiNode)
  {
    var param = this.apiNode[key];
    if(param instanceof AudioParam)
    {
      this.paramPins.push(this.addInputPin(key.replace(/([a-z^])([A-Z])/g, '$1 $2'), [param.defaultValue], VVVV.PinTypes.Value)); //FIXME: params can be connected to a value or an audio stream
      this.paramPins[this.paramPins.length - 1].apiName = key;
    }
  }
}
WebAudioNode.prototype.updateParamPins = function()
{
  var that = this;
  this.paramPins.forEach( function(pin, i)
  {
    if(pin.pinIsChanged() && that.apiNode)
    {
      that.apiNode[pin.apiName].value = pin.getValue(0);
    }
  });
}
WebAudioNode.prototype.updateAudioConnections = function()
{
  var that = this;
  this.audioOutputPins.forEach(function(audioOut, outIndex) {
    if(audioOut.audioConnectionChanged && that.apiNode)
    {
      console.log("Re-connecting!");
      that.apiNode.disconnect(outIndex);
      audioOut.links.forEach(function(link)
      {
        var inPin = link.toPin;
        var inNode = inPin.node;
        var inIndex = inNode.audioInputPins.indexOf(inPin);
        that.apiNode.connect(inNode.apiNode, outIndex, inIndex);
      });
      audioOut.audioConnectionChanged = false;
    }
  });
}

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: AnalyserNode (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.AnalyserNode = function(id, graph) {
  WebAudioNode.call(this, id, 'AnalyserNode (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var that = this;

  var fftSizeIn = this.addInputPin('FFTSize', ['2048'], VVVV.PinTypes.Value);
  var smoothingIn = this.addInputPin('Smoothing', [0.8], VVVV.PinTypes.Value);
  var fftOut = this.addOutputPin('FFT', [], VVVV.PinTypes.Value);
  var fftData;

  function setFFTSize(size)
  {
    if(!size)
      size = 32;
    fftOut.setSliceCount(size);
    fftData = new Float32Array(size);
  }

  setFFTSize(2048);

  this.evaluate = function()
  {
    if(fftSizeIn.pinIsChanged())
      setFFTSize(fftSizeIn.getValue(0));
    if(smoothingIn.pinIsChanged())
      this.apiNode.smoothingTimeConstant = smoothingIn.getValue(0);

    this.updateAudioConnections();
    this.apiNode.getFloatFrequencyData(fftData);
    for(var i = 0; i < fftData.length; i++)
    {
      fftOut.setValue(i, fftData[i]); //FIXME: veeeeery inefficient!
    }

    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.AnalyserNode.prototype = new WebAudioNode('Analyser');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: MediaElementSource (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.MediaElementSource = function(id, graph) {
  WebAudioNode.call(this, id, 'MediaElementSource (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var audioIn = this.addInputPin('Audio', [], this);
  var audioOut = this.addOutputPin('Output', [], VVVV.PinTypes.WebAudio);
  this.audioOutputPins.push(audioOut);

  this.initialize = function() {};

  var mediaElements = [ 7 ];

  this.evaluate = function() {
    this.updateAudioConnections();
    if(audioIn.pinIsChanged())
    {
      var inElement = audioIn.getValue(0);
      if(inElement != mediaElements[0] && inElement)
      {
        mediaElements[0] = inElement;
        this.createAPINode(audioIn.getValue(0));
        inElement.volume = 1;
      }

      if(this.apiNode)
      {
        audioOut.setValue(0, this.apiNode);
      }
    }

  }
}
VVVV.Nodes.MediaElementSource.prototype = new WebAudioNode('MediaElementSource');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: AudioDestination (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.AudioDestination = function(id, graph) {
  WebAudioNode.call(this, id, 'AudioDestination (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  this.createAPINode = function() { this.apiNode = audioContext.destination; };

  this.evaluate = function() {
    this.updateAudioConnections();
  }
}
VVVV.Nodes.AudioDestination.prototype = new WebAudioNode('AudioDestination');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Oscillator (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Oscillator = function(id, graph) {
  WebAudioNode.call(this, id, 'Oscillator (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var typeIn = this.addInputPin("Type", ['sine'], VVVV.PinTypes.Enum);
  typeIn.enumOptions = ['sine', 'square', 'sawtooth', 'triangle', 'custom' ];
  var enableIn = this.addInputPin("Enabled", [1], VVVV.PinTypes.Value);

  this.auto_evaluate = true;

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );

    if(typeIn.pinIsChanged() && this.apiNode)
    {
      this.apiNode.type = typeIn.getValue(0);
    }

    if(enableIn.pinIsChanged() && this.apiNode)
    {
      if(enableIn.getValue(0) > 0)
      {
        this.apiNode.start();
      }
      else
      {
        this.apiNode.stop();
      }
    }
  }
}
VVVV.Nodes.Oscillator.prototype = new WebAudioNode('Oscillator');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Delay (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Delay = function(id, graph) {
  WebAudioNode.call(this, id, 'Delay (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var createAPINode = this.createAPINode;
  this.createAPINode = function() { createAPINode.call(this, 10); }

  this.delays_output = true;

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.Delay.prototype = new WebAudioNode('Delay');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Gain (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Gain = function(id, graph) {
  WebAudioNode.call(this, id, 'Gain (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.Gain.prototype = new WebAudioNode('Gain');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: DynamicsCompressor (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.DynamicsCompressor = function(id, graph) {
  WebAudioNode.call(this, id, 'DynamicsCompressor (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var reductionOut = this.addOutputPin('Reduction', [ 0 ], VVVV.PinTypes.Value);

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );

    //according to the spec, reduction shouldn't be an AudioParam, but browsers seem to implement it as such
    if(this.apiNode)
      reductionOut.setValue(0, this.apiNode.reduction.value);
  }
}
VVVV.Nodes.DynamicsCompressor.prototype = new WebAudioNode('DynamicsCompressor');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: BeatDetector (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.BeatDetector = function(id, graph) {
  WebAudioNode.call(this, id, 'BeatDetector (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var that = this;
  var fftSize = 1024;
  var fftData = new Float32Array(fftSize);
  var beatDetector;

  var beatCounterOut = this.addOutputPin('Beat Counter', [ 0 ], VVVV.PinTypes.Value);
  var bpmOut = this.addOutputPin('BPM', [ 0 ], VVVV.PinTypes.Value);

  var initialize = this.initialize;
  this.initialize = function() {
    initialize.call(this);
    this.apiNode.fftSize = fftSize;
    this.apiNode.smoothingTimeConstant = 0;
  }

  this.evaluate = function()
  {
    if(!beatDetector)
      beatDetector = new BeatDetektor();
    this.updateAudioConnections();
    this.apiNode.getFloatFrequencyData(fftData);
    beatDetector.process(audioContext.currentTime, fftData);
    beatCounterOut.setValue(0, beatDetector.beat_counter);
    bpmOut.setValue(0, beatDetector.win_bpm_int / 10);

    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.BeatDetector.prototype = new WebAudioNode('Analyser');

/*VVVV.Nodes.BiquadFilterNode = makeAudioNodeConstructor('BiquadFilter');
VVVV.Nodes.ChannelMergerNode = makeAudioNodeConstructor('ChannelMerger');
VVVV.Nodes.ChannelSplitterNode = makeAudioNodeConstructor('ChannelSplitter');
VVVV.Nodes.ConvolverNode = makeAudioNodeConstructor('Convolver');
VVVV.Nodes.DelayNode = makeAudioNodeConstructor('Delay');
VVVV.Nodes.DynamicsCompressorNode = makeAudioNodeConstructor('DynamicsCompressor');
VVVV.Nodes.GainNode = makeAudioNodeConstructor('Gain');
VVVV.Nodes.OscillatorNode = makeAudioNodeConstructor('Oscillator');
VVVV.Nodes.PannerNode = makeAudioNodeConstructor('Panner');
VVVV.Nodes.ScriptProcessorNode = makeAudioNodeConstructor('ScriptProcessor');
VVVV.Nodes.WaveShaperNode = makeAudioNodeConstructor('WaveShaper');*/

<commit_msg>Add BiquadFilter (HTML5 Audio)<commit_after>// VVVV.js -- Visual Web Client Programming
// (c) 2011 Matthias Zauner
// VVVV.js is freely distributable under the MIT license.
// Additional authors of sub components are mentioned at the specific code locations.
// This component was developed is (c) 2014 Lukas Winter, distributed under the MIT license.

VVVV.PinTypes.WebAudio = {
  typeName: "WebAudio",
  reset_on_disconnect: true,
  defaultValue: function() {
    return "Unconnected Audio";
  },
  connectionChangedHandlers: {
    'webaudio': function() {
      if(this.direction == PinDirection.Output)
      {
        console.log("Connection changed!", this);
        this.audioConnectionChanged = true;
      }
    }
  }
}

var audioContext = null;

function WebAudioNode(id, name, graph) {
  if(graph) //constructing actual node
  {
    this.constructor(id, name, graph);
    if(!audioContext)
    {
      audioContext = new AudioContext();
    }
    this.initialize = function()
    {
      this.createAPINode();
      this.createAudioPins();
      this.createParamPins();
    }
    this.audioInputPins = [];
    this.audioOutputPins = [];
    this.paramPins = [];
  }
  else //constructing prototype
  {
    this.createAPINode = function(arg)
    {
      //this is just for debugging purposes with firefox's web audio visualiser
      if(id == 'Analyser')
        this.apiNode = audioContext.createAnalyser(arg);
      else if(id == 'MediaElementSource')
        this.apiNode = audioContext.createMediaElementSource(arg);
      else if(id == 'Oscillator')
        this.apiNode = audioContext.createOscillator(arg);
      else if(id == 'Delay')
        this.apiNode = audioContext.createDelay(arg);
      else if(id == 'Gain')
        this.apiNode = audioContext.createGain(arg);
      else if(id == 'DynamicsCompressor')
        this.apiNode = audioContext.createDynamicsCompressor(arg);
      else if(id == 'BiquadFilter')
        this.apiNode = audioContext.createBiquadFilter(arg);
      else //this is the normal code
        this.apiNode = audioContext['create'+id].apply(audioContext, arguments);
    }
    this.auto_evaluate = false;
  }
}
WebAudioNode.prototype = new VVVV.Core.Node();
WebAudioNode.prototype.createAudioPins = function()
{
  for(var i = 0; i < this.apiNode.numberOfInputs; i++)
  {
    this.audioInputPins.push(this.addInputPin('Input '+(i+1), [], VVVV.PinTypes.WebAudio));
  }
  for(var i = 0; i < this.apiNode.numberOfOutputs; i++)
  {
    this.audioOutputPins.push(this.addOutputPin('Output '+(i+1), [this.apiNode], VVVV.PinTypes.WebAudio));
  }
}
WebAudioNode.prototype.createParamPins = function()
{
  for(var key in this.apiNode)
  {
    var param = this.apiNode[key];
    if(param instanceof AudioParam)
    {
      this.paramPins.push(this.addInputPin(key.replace(/([a-z^])([A-Z])/g, '$1 $2'), [param.defaultValue], VVVV.PinTypes.Value)); //FIXME: params can be connected to a value or an audio stream
      this.paramPins[this.paramPins.length - 1].apiName = key;
    }
  }
}
WebAudioNode.prototype.updateParamPins = function()
{
  var that = this;
  this.paramPins.forEach( function(pin, i)
  {
    if(pin.pinIsChanged() && that.apiNode)
    {
      that.apiNode[pin.apiName].value = pin.getValue(0);
    }
  });
}
WebAudioNode.prototype.updateAudioConnections = function()
{
  var that = this;
  this.audioOutputPins.forEach(function(audioOut, outIndex) {
    if(audioOut.audioConnectionChanged && that.apiNode)
    {
      console.log("Re-connecting!");
      that.apiNode.disconnect(outIndex);
      audioOut.links.forEach(function(link)
      {
        var inPin = link.toPin;
        var inNode = inPin.node;
        var inIndex = inNode.audioInputPins.indexOf(inPin);
        that.apiNode.connect(inNode.apiNode, outIndex, inIndex);
      });
      audioOut.audioConnectionChanged = false;
    }
  });
}

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: AnalyserNode (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.AnalyserNode = function(id, graph) {
  WebAudioNode.call(this, id, 'AnalyserNode (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var that = this;

  var fftSizeIn = this.addInputPin('FFTSize', ['2048'], VVVV.PinTypes.Value);
  var smoothingIn = this.addInputPin('Smoothing', [0.8], VVVV.PinTypes.Value);
  var fftOut = this.addOutputPin('FFT', [], VVVV.PinTypes.Value);
  var fftData;

  function setFFTSize(size)
  {
    if(!size)
      size = 32;
    fftOut.setSliceCount(size);
    fftData = new Float32Array(size);
  }

  setFFTSize(2048);

  this.evaluate = function()
  {
    if(fftSizeIn.pinIsChanged())
      setFFTSize(fftSizeIn.getValue(0));
    if(smoothingIn.pinIsChanged())
      this.apiNode.smoothingTimeConstant = smoothingIn.getValue(0);

    this.updateAudioConnections();
    this.apiNode.getFloatFrequencyData(fftData);
    for(var i = 0; i < fftData.length; i++)
    {
      fftOut.setValue(i, fftData[i]); //FIXME: veeeeery inefficient!
    }

    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.AnalyserNode.prototype = new WebAudioNode('Analyser');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: MediaElementSource (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.MediaElementSource = function(id, graph) {
  WebAudioNode.call(this, id, 'MediaElementSource (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var audioIn = this.addInputPin('Audio', [], this);
  var audioOut = this.addOutputPin('Output', [], VVVV.PinTypes.WebAudio);
  this.audioOutputPins.push(audioOut);

  this.initialize = function() {};

  var mediaElements = [ 7 ];

  this.evaluate = function() {
    this.updateAudioConnections();
    if(audioIn.pinIsChanged())
    {
      var inElement = audioIn.getValue(0);
      if(inElement != mediaElements[0] && inElement)
      {
        mediaElements[0] = inElement;
        this.createAPINode(audioIn.getValue(0));
        inElement.volume = 1;
      }

      if(this.apiNode)
      {
        audioOut.setValue(0, this.apiNode);
      }
    }

  }
}
VVVV.Nodes.MediaElementSource.prototype = new WebAudioNode('MediaElementSource');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: AudioDestination (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.AudioDestination = function(id, graph) {
  WebAudioNode.call(this, id, 'AudioDestination (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  this.createAPINode = function() { this.apiNode = audioContext.destination; };

  this.evaluate = function() {
    this.updateAudioConnections();
  }
}
VVVV.Nodes.AudioDestination.prototype = new WebAudioNode('AudioDestination');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Oscillator (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Oscillator = function(id, graph) {
  WebAudioNode.call(this, id, 'Oscillator (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var typeIn = this.addInputPin("Type", ['sine'], VVVV.PinTypes.Enum);
  typeIn.enumOptions = ['sine', 'square', 'sawtooth', 'triangle', 'custom' ];
  var enableIn = this.addInputPin("Enabled", [1], VVVV.PinTypes.Value);

  this.auto_evaluate = true;

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );

    if(typeIn.pinIsChanged() && this.apiNode)
    {
      this.apiNode.type = typeIn.getValue(0);
    }

    if(enableIn.pinIsChanged() && this.apiNode)
    {
      if(enableIn.getValue(0) > 0)
      {
        this.apiNode.start();
      }
      else
      {
        this.apiNode.stop();
      }
    }
  }
}
VVVV.Nodes.Oscillator.prototype = new WebAudioNode('Oscillator');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Delay (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Delay = function(id, graph) {
  WebAudioNode.call(this, id, 'Delay (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var createAPINode = this.createAPINode;
  this.createAPINode = function() { createAPINode.call(this, 10); }

  this.delays_output = true;

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.Delay.prototype = new WebAudioNode('Delay');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: Gain (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.Gain = function(id, graph) {
  WebAudioNode.call(this, id, 'Gain (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.Gain.prototype = new WebAudioNode('Gain');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: BiquadFilter (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.BiquadFilter = function(id, graph) {
  WebAudioNode.call(this, id, 'BiquadFilter (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var typeIn = this.addInputPin("Type", ['lowpass'], VVVV.PinTypes.Enum);
  typeIn.enumOptions = ["lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "peaking", "notch", "allpass" ];

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );

    if(this.apiNode && typeIn.pinIsChanged())
      this.apiNode.type = typeIn.getValue(0);
  }
}
VVVV.Nodes.BiquadFilter.prototype = new WebAudioNode('BiquadFilter');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: DynamicsCompressor (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.DynamicsCompressor = function(id, graph) {
  WebAudioNode.call(this, id, 'DynamicsCompressor (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var reductionOut = this.addOutputPin('Reduction', [ 0 ], VVVV.PinTypes.Value);

  this.evaluate = function() {
    this.updateAudioConnections();
    this.updateParamPins();
    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );

    //according to the spec, reduction shouldn't be an AudioParam, but browsers seem to implement it as such
    if(this.apiNode)
      reductionOut.setValue(0, this.apiNode.reduction.value);
  }
}
VVVV.Nodes.DynamicsCompressor.prototype = new WebAudioNode('DynamicsCompressor');

/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 NODE: BeatDetector (HTML5 Audio)
 Author(s): 'Lukas Winter'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/

VVVV.Nodes.BeatDetector = function(id, graph) {
  WebAudioNode.call(this, id, 'BeatDetector (HTML5 Audio)', graph);

  this.meta = {
    authors: ['Lukas Winter'],
    original_authors: [],
    credits: [],
    compatibility_issues: []
  };

  var that = this;
  var fftSize = 1024;
  var fftData = new Float32Array(fftSize);
  var beatDetector;

  var beatCounterOut = this.addOutputPin('Beat Counter', [ 0 ], VVVV.PinTypes.Value);
  var bpmOut = this.addOutputPin('BPM', [ 0 ], VVVV.PinTypes.Value);

  var initialize = this.initialize;
  this.initialize = function() {
    initialize.call(this);
    this.apiNode.fftSize = fftSize;
    this.apiNode.smoothingTimeConstant = 0;
  }

  this.evaluate = function()
  {
    if(!beatDetector)
      beatDetector = new BeatDetektor();
    this.updateAudioConnections();
    this.apiNode.getFloatFrequencyData(fftData);
    beatDetector.process(audioContext.currentTime, fftData);
    beatCounterOut.setValue(0, beatDetector.beat_counter);
    bpmOut.setValue(0, beatDetector.win_bpm_int / 10);

    this.audioOutputPins.forEach( function(pin) { pin.markPinAsChanged(); } );
  }
}
VVVV.Nodes.BeatDetector.prototype = new WebAudioNode('Analyser');

/*VVVV.Nodes.BiquadFilterNode = makeAudioNodeConstructor('BiquadFilter');
VVVV.Nodes.ChannelMergerNode = makeAudioNodeConstructor('ChannelMerger');
VVVV.Nodes.ChannelSplitterNode = makeAudioNodeConstructor('ChannelSplitter');
VVVV.Nodes.ConvolverNode = makeAudioNodeConstructor('Convolver');
VVVV.Nodes.DelayNode = makeAudioNodeConstructor('Delay');
VVVV.Nodes.DynamicsCompressorNode = makeAudioNodeConstructor('DynamicsCompressor');
VVVV.Nodes.GainNode = makeAudioNodeConstructor('Gain');
VVVV.Nodes.OscillatorNode = makeAudioNodeConstructor('Oscillator');
VVVV.Nodes.PannerNode = makeAudioNodeConstructor('Panner');
VVVV.Nodes.ScriptProcessorNode = makeAudioNodeConstructor('ScriptProcessor');
VVVV.Nodes.WaveShaperNode = makeAudioNodeConstructor('WaveShaper');*/

<|endoftext|>


<commit_before>!function ($) {
  "use strict";
  // version: 2.7
  // by Mattia Larentis - follow me on twitter! @SpiritualGuru

  var addToAttribute = function (obj, array, value) {
    var i = 0
      , length = array.length;

    for (; i < length; i++) {
      obj = obj[array[i]] = obj[array[i]] || i == ( length - 1) ? value : {}
    }
  };

  $.fn.toggleButtons = function (method) {
    var $element
      , $div
      , transitionSpeed = 0.05
      , methods = {
        init: function (opt) {
          this.each(function () {
              var $spanLeft
                , $spanRight
                , options
                , moving
                , dataAttribute = {};

              $element = $(this);
              $element.addClass('toggle-button');

              $.each($element.data(), function (i, el) {
                var key
                  , tmp = {};

                if (i.indexOf("togglebutton") === 0) {
                  key = i.match(/[A-Z][a-z]+/g);
                  key = $.map(key, function (n) {
                    return (n.toLowerCase());
                  });

                  addToAttribute(tmp, key, el);
                  dataAttribute = $.extend(true, dataAttribute, tmp);
                }
              });

              options = $.extend(true, {}, $.fn.toggleButtons.defaults, opt, dataAttribute);

              $(this).data('options', options);

              $spanLeft = $('').addClass("labelLeft").text(options.label.enabled === undefined ? "ON" : options.label.enabled);
              $spanRight = $('').addClass("labelRight").text(options.label.disabled === undefined ? "OFF " : options.label.disabled);

              // html layout
              $div = $element.find('input:checkbox').wrap($('

')).parent();
              $div.append($spanLeft);
              $div.append($('').attr('for', $element.find('input').attr('id')));
              $div.append($spanRight);

              if ($element.find('input').is(':checked'))
                $element.find('>div').css('left', "0");
              else $element.find('>div').css('left', "-50%");

              if (options.animated) {
                if (options.transitionspeed !== undefined)
                  if (/^(\d*%$)/.test(options.transitionspeed))  // is a percent value?
                    transitionSpeed = 0.05 * parseInt(options.transitionspeed) / 100;
                  else
                    transitionSpeed = options.transitionspeed;
              }
              else transitionSpeed = 0;

              $(this).data('transitionSpeed', transitionSpeed * 1000);

              options["width"] /= 2;

              // width of the bootstrap-toggle-button
              $element
                .css('width', options.width * 2)
                .find('>div').css('width', options.width * 3)
                .find('>span, >label').css('width', options.width);

              // height of the bootstrap-toggle-button
              $element
                .css('height', options.height)
                .find('span, label')
                .css('height', options.height)
                .filter('span')
                .css('line-height', options.height + "px");

              if ($element.find('input').is(':disabled'))
                $(this).addClass('deactivate');

              $element.find('span').css(options.font);

              // enabled custom color
              if (options.style.enabled === undefined) {
                if (options.style.custom !== undefined && options.style.custom.enabled !== undefined && options.style.custom.enabled.background !== undefined) {
                  $spanLeft.css('color', options.style.custom.enabled.color);
                  if (options.style.custom.enabled.gradient === undefined)
                    $spanLeft.css('background', options.style.custom.enabled.background);
                  else $.each(["-webkit-", "-moz-", "-o-", ""], function (i, el) {
                    $spanLeft.css('background-image', el + 'linear-gradient(top, ' + options.style.custom.enabled.background + ',' + options.style.custom.enabled.gradient + ')');
                  });
                }
              }
              else $spanLeft.addClass(options.style.enabled);

              // disabled custom color
              if (options.style.disabled === undefined) {
                if (options.style.custom !== undefined && options.style.custom.disabled !== undefined && options.style.custom.disabled.background !== undefined) {
                  $spanRight.css('color', options.style.custom.disabled.color);
                  if (options.style.custom.disabled.gradient === undefined)
                    $spanRight.css('background', options.style.custom.disabled.background);
                  else $.each(["-webkit-", "-moz-", "-o-", ""], function (i, el) {
                    $spanRight.css('background-image', el + 'linear-gradient(top, ' + options.style.custom.disabled.background + ',' + options.style.custom.disabled.gradient + ')');
                  });
                }
              }
              else $spanRight.addClass(options.style.disabled);

              var changeStatus = function ($this) {
                $this.siblings('label')
                  .trigger('mousedown')
                  .trigger('mouseup')
                  .trigger('click');
              };

              $spanLeft.on('click', function (e) {
                changeStatus($(this));
              });
              $spanRight.on('click', function (e) {
                changeStatus($(this));
              });

              $('.toggle-button').find('input').on('change', function (e, skipOnChange) {
                var $element = $(this).parent()
                  , active = $(this).is(':checked')
                  , $toggleButton = $(this).closest('.toggle-button');

                e.preventDefault();
                e.stopImmediatePropagation();

                $element.stop().animate({'left': active ? '0' : '-50%'}, $toggleButton.data('transitionSpeed'));

                options = $toggleButton.data('options');

                if (!skipOnChange)
                  options.onChange($element, active, e);
              });

              $('.toggle-button').find('label').on('mousedown touchstart', function (e) {
                moving = false;
                e.preventDefault();
                e.stopImmediatePropagation();

                if ($(this).closest('.toggle-button').is('.deactivate'))
                  $(this).off('click');
                else {
                  $(this).on('mousemove touchmove', function (e) {
                    var $element = $(this).closest('.toggle-button')
                      , relativeX = (e.pageX || e.originalEvent.targetTouches[0].pageX) - $element.offset().left
                      , percent = ((relativeX / (options.width * 2)) * 100);
                    moving = true;

                    e.stopImmediatePropagation();
                    e.preventDefault();

                    if (percent < 25)
                      percent = 25;
                    else if (percent > 75)
                      percent = 75;

                    $element.find('>div').css('left', (percent - 75) + "%");
                  });

                  $(this).on('click touchend', function (e) {
                    var $target = $(e.target)
                      , $myCheckBox = $target.siblings('input');

                    e.stopImmediatePropagation();
                    e.preventDefault();
                    $(this).off('mouseleave');

                    if (moving)
                      if (parseInt($(this).parent().css('left')) < -25)
                        $myCheckBox.attr('checked', false);
                      else $myCheckBox.attr('checked', true);
                    else $myCheckBox.attr("checked", !$myCheckBox.is(":checked"));

                    $myCheckBox.trigger('change');
                  });

                  $(this).on('mouseleave', function (e) {
                    var $myCheckBox = $(this).siblings('input');

                    e.preventDefault();
                    e.stopImmediatePropagation();

                    $(this).off('mouseleave');
                    $(this).trigger('mouseup');

                    if (parseInt($(this).parent().css('left')) < -25)
                      $myCheckBox.attr('checked', false);
                    else $myCheckBox.attr('checked', true);

                    $myCheckBox.trigger('change');
                  });

                  $(this).on('mouseup', function (e) {
                    e.stopImmediatePropagation();
                    e.preventDefault();
                    $(this).off('mousemove');
                  });
                }
              });
            }
          );
          return this;
        },
        toggleActivation: function () {
          $(this).toggleClass('deactivate');
        },
        toggleState: function (skipOnChange) {
          var $input = $(this).find('input');
          $input.attr('checked', !$input.is(':checked')).trigger('change', skipOnChange);
        },
        setState: function(value, skipOnChange) {
          $(this).find('input').attr('checked', value).trigger('change', skipOnChange);
        },
        status: function () {
          return $(this).find('input:checkbox').is(':checked');
        }
      };

    if (methods[method])
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    else if (typeof method === 'object' || !method)
      return methods.init.apply(this, arguments);
    else
      $.error('Method ' + method + ' does not exist!');
  };

  $.fn.toggleButtons.defaults = {
    onChange: function () {
    },
    width: 100,
    height: 25,
    font: {},
    animated: true,
    transitionspeed: undefined,
    label: {
      enabled: undefined,
      disabled: undefined
    },
    style: {
      enabled: undefined,
      disabled: undefined,
      custom: {
        enabled: {
          background: undefined,
          gradient: undefined,
          color: "#FFFFFF"
        },
        disabled: {
          background: undefined,
          gradient: undefined,
          color: "#FFFFFF"
        }
      }
    }
  };
}($);
<commit_msg>when using custom background gradient, also set background-color for usage in IE9. TODO: update minified version.<commit_after>!function ($) {
  "use strict";
  // version: 2.7
  // by Mattia Larentis - follow me on twitter! @SpiritualGuru

  var addToAttribute = function (obj, array, value) {
    var i = 0
      , length = array.length;

    for (; i < length; i++) {
      obj = obj[array[i]] = obj[array[i]] || i == ( length - 1) ? value : {}
    }
  };

  $.fn.toggleButtons = function (method) {
    var $element
      , $div
      , transitionSpeed = 0.05
      , methods = {
        init: function (opt) {
          this.each(function () {
              var $spanLeft
                , $spanRight
                , options
                , moving
                , dataAttribute = {};

              $element = $(this);
              $element.addClass('toggle-button');

              $.each($element.data(), function (i, el) {
                var key
                  , tmp = {};

                if (i.indexOf("togglebutton") === 0) {
                  key = i.match(/[A-Z][a-z]+/g);
                  key = $.map(key, function (n) {
                    return (n.toLowerCase());
                  });

                  addToAttribute(tmp, key, el);
                  dataAttribute = $.extend(true, dataAttribute, tmp);
                }
              });

              options = $.extend(true, {}, $.fn.toggleButtons.defaults, opt, dataAttribute);

              $(this).data('options', options);

              $spanLeft = $('').addClass("labelLeft").text(options.label.enabled === undefined ? "ON" : options.label.enabled);
              $spanRight = $('').addClass("labelRight").text(options.label.disabled === undefined ? "OFF " : options.label.disabled);

              // html layout
              $div = $element.find('input:checkbox').wrap($('

')).parent();
              $div.append($spanLeft);
              $div.append($('').attr('for', $element.find('input').attr('id')));
              $div.append($spanRight);

              if ($element.find('input').is(':checked'))
                $element.find('>div').css('left', "0");
              else $element.find('>div').css('left', "-50%");

              if (options.animated) {
                if (options.transitionspeed !== undefined)
                  if (/^(\d*%$)/.test(options.transitionspeed))  // is a percent value?
                    transitionSpeed = 0.05 * parseInt(options.transitionspeed) / 100;
                  else
                    transitionSpeed = options.transitionspeed;
              }
              else transitionSpeed = 0;

              $(this).data('transitionSpeed', transitionSpeed * 1000);

              options["width"] /= 2;

              // width of the bootstrap-toggle-button
              $element
                .css('width', options.width * 2)
                .find('>div').css('width', options.width * 3)
                .find('>span, >label').css('width', options.width);

              // height of the bootstrap-toggle-button
              $element
                .css('height', options.height)
                .find('span, label')
                .css('height', options.height)
                .filter('span')
                .css('line-height', options.height + "px");

              if ($element.find('input').is(':disabled'))
                $(this).addClass('deactivate');

              $element.find('span').css(options.font);

              // enabled custom color
              if (options.style.enabled === undefined) {
                if (options.style.custom !== undefined && options.style.custom.enabled !== undefined && options.style.custom.enabled.background !== undefined) {
                  $spanLeft.css('color', options.style.custom.enabled.color);
                  if (options.style.custom.enabled.gradient === undefined)
                    $spanLeft.css('background', options.style.custom.enabled.background);
                  else $.each(["-webkit-", "-moz-", "-o-", ""], function (i, el) {
                    $spanLeft.css('background-color', options.style.custom.enabled.background);
                    $spanLeft.css('background-image', el + 'linear-gradient(top, ' + options.style.custom.enabled.background + ',' + options.style.custom.enabled.gradient + ')');
                  });
                }
              }
              else $spanLeft.addClass(options.style.enabled);

              // disabled custom color
              if (options.style.disabled === undefined) {
                if (options.style.custom !== undefined && options.style.custom.disabled !== undefined && options.style.custom.disabled.background !== undefined) {
                  $spanRight.css('color', options.style.custom.disabled.color);
                  if (options.style.custom.disabled.gradient === undefined)
                    $spanRight.css('background', options.style.custom.disabled.background);
                  else $.each(["-webkit-", "-moz-", "-o-", ""], function (i, el) {
                    $spanRight.css('background-color', options.style.custom.disabled.background);
                    $spanRight.css('background-image', el + 'linear-gradient(top, ' + options.style.custom.disabled.background + ',' + options.style.custom.disabled.gradient + ')');
                  });
                }
              }
              else $spanRight.addClass(options.style.disabled);

              var changeStatus = function ($this) {
                $this.siblings('label')
                  .trigger('mousedown')
                  .trigger('mouseup')
                  .trigger('click');
              };

              $spanLeft.on('click', function (e) {
                changeStatus($(this));
              });
              $spanRight.on('click', function (e) {
                changeStatus($(this));
              });

              $('.toggle-button').find('input').on('change', function (e, skipOnChange) {
                var $element = $(this).parent()
                  , active = $(this).is(':checked')
                  , $toggleButton = $(this).closest('.toggle-button');

                e.preventDefault();
                e.stopImmediatePropagation();

                $element.stop().animate({'left': active ? '0' : '-50%'}, $toggleButton.data('transitionSpeed'));

                options = $toggleButton.data('options');

                if (!skipOnChange)
                  options.onChange($element, active, e);
              });

              $('.toggle-button').find('label').on('mousedown touchstart', function (e) {
                moving = false;
                e.preventDefault();
                e.stopImmediatePropagation();

                if ($(this).closest('.toggle-button').is('.deactivate'))
                  $(this).off('click');
                else {
                  $(this).on('mousemove touchmove', function (e) {
                    var $element = $(this).closest('.toggle-button')
                      , relativeX = (e.pageX || e.originalEvent.targetTouches[0].pageX) - $element.offset().left
                      , percent = ((relativeX / (options.width * 2)) * 100);
                    moving = true;

                    e.stopImmediatePropagation();
                    e.preventDefault();

                    if (percent < 25)
                      percent = 25;
                    else if (percent > 75)
                      percent = 75;

                    $element.find('>div').css('left', (percent - 75) + "%");
                  });

                  $(this).on('click touchend', function (e) {
                    var $target = $(e.target)
                      , $myCheckBox = $target.siblings('input');

                    e.stopImmediatePropagation();
                    e.preventDefault();
                    $(this).off('mouseleave');

                    if (moving)
                      if (parseInt($(this).parent().css('left')) < -25)
                        $myCheckBox.attr('checked', false);
                      else $myCheckBox.attr('checked', true);
                    else $myCheckBox.attr("checked", !$myCheckBox.is(":checked"));

                    $myCheckBox.trigger('change');
                  });

                  $(this).on('mouseleave', function (e) {
                    var $myCheckBox = $(this).siblings('input');

                    e.preventDefault();
                    e.stopImmediatePropagation();

                    $(this).off('mouseleave');
                    $(this).trigger('mouseup');

                    if (parseInt($(this).parent().css('left')) < -25)
                      $myCheckBox.attr('checked', false);
                    else $myCheckBox.attr('checked', true);

                    $myCheckBox.trigger('change');
                  });

                  $(this).on('mouseup', function (e) {
                    e.stopImmediatePropagation();
                    e.preventDefault();
                    $(this).off('mousemove');
                  });
                }
              });
            }
          );
          return this;
        },
        toggleActivation: function () {
          $(this).toggleClass('deactivate');
        },
        toggleState: function (skipOnChange) {
          var $input = $(this).find('input');
          $input.attr('checked', !$input.is(':checked')).trigger('change', skipOnChange);
        },
        setState: function(value, skipOnChange) {
          $(this).find('input').attr('checked', value).trigger('change', skipOnChange);
        },
        status: function () {
          return $(this).find('input:checkbox').is(':checked');
        }
      };

    if (methods[method])
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    else if (typeof method === 'object' || !method)
      return methods.init.apply(this, arguments);
    else
      $.error('Method ' + method + ' does not exist!');
  };

  $.fn.toggleButtons.defaults = {
    onChange: function () {
    },
    width: 100,
    height: 25,
    font: {},
    animated: true,
    transitionspeed: undefined,
    label: {
      enabled: undefined,
      disabled: undefined
    },
    style: {
      enabled: undefined,
      disabled: undefined,
      custom: {
        enabled: {
          background: undefined,
          gradient: undefined,
          color: "#FFFFFF"
        },
        disabled: {
          background: undefined,
          gradient: undefined,
          color: "#FFFFFF"
        }
      }
    }
  };
}($);
<|endoftext|>