<commit_before>/*    Copyright 2016 Firewalla LLC
 *
 *    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 Sensor = require('./Sensor.js').Sensor;

const sem = require('../sensor/SensorEventManager.js').getInstance();

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

const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsConfigFolder = `${userConfigFolder}/dns`;
const safeSearchConfigFile = `${dnsConfigFolder}/safeSearch.conf`;

const devicemasqConfigFolder = `${userConfigFolder}/devicemasq`;

const pidFile = `${f.getRuntimeInfoFolder()}/safeSearch.dnsmasq.pid`;

const configKey = "ext.safeSearch.config";

const updateInterval = 3600 * 1000 // once per hour

const rclient = require('../util/redis_manager.js').getRedisClient();

const domainBlock = require('../control/DomainBlock.js')();

const fs = require('fs');
const Promise = require('bluebird');
Promise.promisifyAll(fs);

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

const SysManager = require('../net2/SysManager');
const sysManager = new SysManager();

const exec = require('child-process-promise').exec;

const networkTool = require('../net2/NetworkTool')();

const safeSearchDNSPort = 8863;

const iptables = require('../net2/Iptables');

const fc = require('../net2/config.js');

class SafeSearchPlugin extends Sensor {

  async run() {

    this.systemSwitch = false;
    this.adminSystemSwitch = false;
    this.enabledMacAddresses = {};

    this.domainCaches = {};

    const exists = await this.configExists();

    if(!exists) {
      await this.setDefaultSafeSearchConfig();
    }

    extensionManager.registerExtension("safeSearch", this, {
      applyPolicy: this.applyPolicy,
      start: this.start,
      stop: this.stop
    });

    await exec(`mkdir -p ${devicemasqConfigFolder}`);

    sem.once('IPTABLES_READY', async () => {
      if(fc.isFeatureOn("safe_search")) {
        await this.globalOn();
      } else {
        await this.globalOff();
      }

      fc.onFeature("safe_search", async (feature, status) => {
        if(feature !== "safe_search") {
          return;
        }

        if(status) {
          await this.globalOn();
        } else {
          await this.globalOff();
        }
      })

      sem.on('SAFESEARCH_REFRESH', (event) => {
        this.applySafeSearch();
      })
    })

    await this.job();
    this.timer = setInterval(async () => {
      return this.job();
    }, this.config.refreshInterval || 3600 * 1000); // one hour by default
  }

  async job() {
    await this.updateAllDomains();
    await this.applySafeSearch();
  }

  async apiRun() {
    extensionManager.onSet("safeSearchConfig", async (msg, data) => {
      await rclient.setAsync(configKey, JSON.stringify(data));
      sem.sendEventToFireMain({
        type: 'SAFESEARCH_REFRESH'
      });
    });

    extensionManager.onGet("safeSearchConfig", async (msg, data) => {
      return this.getSafeSearchConfig();
    });
  }

  async getSafeSearchConfig() {
    const json = await rclient.getAsync(configKey);
    try {
      return JSON.parse(json);
    } catch(err) {
      log.error(`Got error when loading config from ${configKey}`);
      return {};
    }
  }

  async setDefaultSafeSearchConfig() {
    if(this.config && this.config.defaultConfig) {
      log.info("Setting default safe search config...");
      return rclient.setAsync(configKey, JSON.stringify(this.config.defaultConfig));
    }
  }

  async configExists() {
    const check = await rclient.typeAsync(configKey);
    return check !== 'none';
  }

  async applyPolicy(host, ip, policy) {
    log.info("Applying policy:", ip, policy)

    try {
      if(ip === '0.0.0.0') {
        if(policy && policy.state) {
          this.systemSwitch = true;
        } else {
          this.systemSwitch = false;
        }
        return this.applySystemSafeSearch();
      } else {
        const macAddress = host && host.o && host.o.mac;
        if(macAddress) {
          if(policy && policy.state) {
            this.enabledMacAddresses[macAddress] = 1;
          } else {
            delete this.enabledMacAddresses[macAddress];
          }
          return this.applyDeviceSafeSearch(macAddress);
        }
      }
    } catch(err) {
      log.error("Got error when applying safesearch policy", err);
    }

  }

  // {
  //   google: "on", // "on", "off"
  //   youtube: "strict", // "strict", "moderate", "off"
  //   bing: "on" // "on", "off"
  // }

  getMappingKey(type, value) {
    switch(type) {
    case "youtube":
      switch(value) {
        case "on":
        case "strict":
          return "youtube_strict";
        case "moderate":
          return "youtube_moderate";
        default:
          return null;
      }
    default:
      return type;
    }
  }

  getMappingResult(key) {
    if(key) {
      const mapping = this.getDomainMapping();
      if(!mapping) {
        return null;
      }
      return mapping[key];
    } else {
      return null;
    }
  }

  getDomainMapping() {
    return this.config && this.config.mapping;
  }

  getDNSMasqEntry(macAddress, ipAddress, domainToBeRedirect) {
    if(macAddress) {
      return `address=/${domainToBeRedirect}/${ipAddress}%${macAddress.toUpperCase()}`;
    } else {
      return `address=/${domainToBeRedirect}/${ipAddress}`;
    }

  }

  async loadDomainCache(domain) {
    const key = `rdns:domain:${domain}`;
    let results = await rclient.zrevrangeAsync(key, -1, -1);
    results = results.filter((ip) => !f.isReservedBlockingIP(ip));
    if(results.length > 0) {
      log.info(`Domain ${domain} ======> ${results[0]}`);
      return results[0];
    }

    return null;
  }

  async updateDomainCache(domain) {
    return domainBlock.resolveDomain(domain);
  }

  getAllDomains() {
    const mappings = this.getDomainMapping();
    if(mappings) {
      const values = Object.values(mappings);
      const domains = [];
      values.forEach((value) => {
        if(value) {
          domains.push(...Object.keys(value));
        }
      });
      return domains;
    } else {
      return [];
    }
  }

  async updateAllDomains() {
    log.info("Updating all domains...");
    return Promise.all(this.getAllDomains().map(async domain => this.updateDomainCache(domain)));
  }

  // redirect targetDomain to the ip address of safe domain
  async generateConfig(macAddress, safeDomain, targetDomains) {
    const ip = await this.loadDomainCache(safeDomain);
    if(ip) {
      return targetDomains.map((targetDomain) => {
        return this.getDNSMasqEntry(macAddress, ip, targetDomain);
      });
    } else {
      return [];
    }
  }

  async applySafeSearch() {
    await this.applySystemSafeSearch();

    for(const macAddress in this.enabledMacAddresses) {
      await this.applyDeviceSafeSearch(macAddress)
    }
  }

  async applySystemSafeSearch() {
    if(this.systemSwitch && this.adminSystemSwitch) {
      const config = await this.getSafeSearchConfig();
      return this.systemStart(config);
    } else {
      return this.systemStop();
    }
  }

  async applyDeviceSafeSearch(macAddress) {
    try {
      if(this.enabledMacAddresses[macAddress]) {
        const config = await this.getSafeSearchConfig();
        return this.perDeviceStart(macAddress, config)
      } else {
        return this.perDeviceStop(macAddress);
      }
    } catch(err) {
      log.error(`Failed to apply safe search on device ${macAddress}, err: ${err}`);
    }
  }

  async generateConfigFile(macAddress, config) {
    let entries = [];

    for(const type in config) {
      const value = config[type];
      if(value === 'off') {
        continue;
      }

      const key = this.getMappingKey(type, value);
      const result = this.getMappingResult(key);
      if(result) {
        const safeDomains = Object.keys(result);
        await Promise.all(safeDomains.map(async (safeDomain) => {
          const targetDomains = result[safeDomain];
          const configs = await this.generateConfig(macAddress, safeDomain, targetDomains);
          entries.push(...configs);
        }));
      }
    }

    entries.push("");

    //await fs.writeFileAsync(destinationFile, entries.join("\n"));
    return entries.join("\n");
  }

  async saveConfigFile(file, content) {
    return fs.writeFileAsync(file, content);
  }

  async loadConfigFile(file) {
    return fs.readFileAsync(file, {encoding: 'utf8'});
  }

  async deleteConfigFile(destinationFile) {
    return fs.unlinkAsync(destinationFile).catch(() => undefined);
  }

  // system level

  getConfigFile(macAddress) {
    if(macAddress) {
      return `${devicemasqConfigFolder}/safeSearch_${macAddress}.conf`;
    } else {
      return safeSearchConfigFile;
    }
  }

  async systemStart(config) {
    const configString = await this.generateConfigFile(undefined, config);
    const existingString = await this.loadConfigFile(this.getConfigFile()).catch((err) => null);
    if(configString !== existingString || existingString === null) {
      await this.saveConfigFile(this.getConfigFile(), configString);
      await dnsmasq.start(true);
    }
    return;
  }

  async systemStop() {
    await this.deleteConfigFile(safeSearchConfigFile);
    await dnsmasq.start(true);
  }

  /*
   * Safe Search DNS server will use local primary dns server as upstream server
   */
  async startDeviceMasq() {
    if(this.starting) {
      return;
    }

    try {
      this.starting = true;
      await this.delay(5000);
      this.starting = false;
      return exec("sudo systemctl restart devicemasq");
    } catch(err) {
      log.error(`Failed to restart devicemasq, err: ${err}`);
    }
  }

  async stopDeviceMasq() {
    return exec("sudo systemctl stop devicemasq");
  }

  async isDeviceMasqRunning() {
    try {
      await exec("systemctl is-active devicemasq");
    } catch(err) {
      return false;
    }

    return true;
  }

  async perDeviceStart(mac, config) {
    const configString = await this.generateConfigFile(mac, config);
    const existingString = await this.loadConfigFile(this.getConfigFile(mac)).catch((err) => null);
    if(configString !== existingString || existingString === null) {
      await this.saveConfigFile(this.getConfigFile(mac), configString);
      await this.startDeviceMasq();
    }
    await this.delay(8 * 1000); // wait for a while before activating the dns redirect
    await exec(`sudo ipset -! add devicedns_mac_set ${mac}`);
  }

  async perDeviceStop(mac) {
    await exec(`sudo ipset -! del devicedns_mac_set ${mac}`);
    const file = this.getConfigFile(mac);
    await this.deleteConfigFile(file);
    await this.startDeviceMasq();
  }

  // global on/off
  async globalOn() {
    await this.addIptablesRules();
    this.adminSystemSwitch = true;
    await this.applySystemSafeSearch();
  }

  async globalOff() {
    await this.removeIptablesRules();
    this.adminSystemSwitch = false;
    await this.applySystemSafeSearch();
  }

  async addIptablesRules() {
    const localIP = sysManager.myIp();
    const deviceDNS = `${localIP}:8863`;

    const ipv6s = sysManager.myIp6();

    for(const protocol of ["tcp", "udp"]) {
      const deviceDNSRule = `sudo iptables -w -t nat -I PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS}`;
      const cmd = iptables.wrapIptables(deviceDNSRule);
      await exec(cmd).catch(() => undefined);

      for(const ip6 of ipv6s) {
        if (ip6.startsWith("fe80::")) {
          // use local link ipv6 for port forwarding, both ipv4 and v6 dns traffic should go through dnsmasq

          const deviceDNS6 = `[${ip6}]:8863`;

          const deviceDNSRule = `sudo ip6tables -w -t nat -I PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS6}`;
          const cmd = iptables.wrapIptables(deviceDNSRule);
          await exec(cmd).catch(() => undefined);
        }
      }
    }
  }

  async removeIptablesRules() {
    const localIP = sysManager.myIp();
    const deviceDNS = `${localIP}:8863`;

    const ipv6s = sysManager.myIp6();

    for(const protocol of ["tcp", "udp"]) {
      const deviceDNSRule = `sudo iptables -w -t nat -D PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS}`;
      const cmd = iptables.wrapIptables(deviceDNSRule);
      await exec(cmd).catch(() => undefined);

      for(const ip6 of ipv6s) {
        if (ip6.startsWith("fe80::")) {
          // use local link ipv6 for port forwarding, both ipv4 and v6 dns traffic should go through dnsmasq

          const deviceDNS6 = `[${ip6}]:8863`;

          const deviceDNSRule = `sudo ip6tables -w -t nat -D PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS6}`;
          const cmd = iptables.wrapIptables(deviceDNSRule);
          await exec(cmd).catch(() => undefined);
        }
      }
    }
  }
}

module.exports = SafeSearchPlugin
<commit_msg>BUG: should use ipv4 address as priority when setting safe search ip<commit_after>/*    Copyright 2016 Firewalla LLC
 *
 *    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 Sensor = require('./Sensor.js').Sensor;

const sem = require('../sensor/SensorEventManager.js').getInstance();

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

const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsConfigFolder = `${userConfigFolder}/dns`;
const safeSearchConfigFile = `${dnsConfigFolder}/safeSearch.conf`;

const devicemasqConfigFolder = `${userConfigFolder}/devicemasq`;

const pidFile = `${f.getRuntimeInfoFolder()}/safeSearch.dnsmasq.pid`;

const configKey = "ext.safeSearch.config";

const updateInterval = 3600 * 1000 // once per hour

const rclient = require('../util/redis_manager.js').getRedisClient();

const domainBlock = require('../control/DomainBlock.js')();

const fs = require('fs');
const Promise = require('bluebird');
Promise.promisifyAll(fs);

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

const SysManager = require('../net2/SysManager');
const sysManager = new SysManager();

const exec = require('child-process-promise').exec;

const networkTool = require('../net2/NetworkTool')();

const safeSearchDNSPort = 8863;

const iptables = require('../net2/Iptables');

const fc = require('../net2/config.js');

const iptool = require('ip')

class SafeSearchPlugin extends Sensor {

  async run() {

    this.systemSwitch = false;
    this.adminSystemSwitch = false;
    this.enabledMacAddresses = {};

    this.domainCaches = {};

    const exists = await this.configExists();

    if(!exists) {
      await this.setDefaultSafeSearchConfig();
    }

    extensionManager.registerExtension("safeSearch", this, {
      applyPolicy: this.applyPolicy,
      start: this.start,
      stop: this.stop
    });

    await exec(`mkdir -p ${devicemasqConfigFolder}`);

    sem.once('IPTABLES_READY', async () => {
      if(fc.isFeatureOn("safe_search")) {
        await this.globalOn();
      } else {
        await this.globalOff();
      }

      fc.onFeature("safe_search", async (feature, status) => {
        if(feature !== "safe_search") {
          return;
        }

        if(status) {
          await this.globalOn();
        } else {
          await this.globalOff();
        }
      })

      sem.on('SAFESEARCH_REFRESH', (event) => {
        this.applySafeSearch();
      })
    })

    await this.job();
    this.timer = setInterval(async () => {
      return this.job();
    }, this.config.refreshInterval || 3600 * 1000); // one hour by default
  }

  async job() {
    await this.updateAllDomains();
    await this.applySafeSearch();
  }

  async apiRun() {
    extensionManager.onSet("safeSearchConfig", async (msg, data) => {
      await rclient.setAsync(configKey, JSON.stringify(data));
      sem.sendEventToFireMain({
        type: 'SAFESEARCH_REFRESH'
      });
    });

    extensionManager.onGet("safeSearchConfig", async (msg, data) => {
      return this.getSafeSearchConfig();
    });
  }

  async getSafeSearchConfig() {
    const json = await rclient.getAsync(configKey);
    try {
      return JSON.parse(json);
    } catch(err) {
      log.error(`Got error when loading config from ${configKey}`);
      return {};
    }
  }

  async setDefaultSafeSearchConfig() {
    if(this.config && this.config.defaultConfig) {
      log.info("Setting default safe search config...");
      return rclient.setAsync(configKey, JSON.stringify(this.config.defaultConfig));
    }
  }

  async configExists() {
    const check = await rclient.typeAsync(configKey);
    return check !== 'none';
  }

  async applyPolicy(host, ip, policy) {
    log.info("Applying policy:", ip, policy)

    try {
      if(ip === '0.0.0.0') {
        if(policy && policy.state) {
          this.systemSwitch = true;
        } else {
          this.systemSwitch = false;
        }
        return this.applySystemSafeSearch();
      } else {
        const macAddress = host && host.o && host.o.mac;
        if(macAddress) {
          if(policy && policy.state) {
            this.enabledMacAddresses[macAddress] = 1;
          } else {
            delete this.enabledMacAddresses[macAddress];
          }
          return this.applyDeviceSafeSearch(macAddress);
        }
      }
    } catch(err) {
      log.error("Got error when applying safesearch policy", err);
    }

  }

  // {
  //   google: "on", // "on", "off"
  //   youtube: "strict", // "strict", "moderate", "off"
  //   bing: "on" // "on", "off"
  // }

  getMappingKey(type, value) {
    switch(type) {
    case "youtube":
      switch(value) {
        case "on":
        case "strict":
          return "youtube_strict";
        case "moderate":
          return "youtube_moderate";
        default:
          return null;
      }
    default:
      return type;
    }
  }

  getMappingResult(key) {
    if(key) {
      const mapping = this.getDomainMapping();
      if(!mapping) {
        return null;
      }
      return mapping[key];
    } else {
      return null;
    }
  }

  getDomainMapping() {
    return this.config && this.config.mapping;
  }

  getDNSMasqEntry(macAddress, ipAddress, domainToBeRedirect) {
    if(macAddress) {
      return `address=/${domainToBeRedirect}/${ipAddress}%${macAddress.toUpperCase()}`;
    } else {
      return `address=/${domainToBeRedirect}/${ipAddress}`;
    }

  }

  async loadDomainCache(domain) {
    const key = `rdns:domain:${domain}`;
    let results = await rclient.zrevrangeAsync(key, -1, -1);
    results = results.filter((ip) => !f.isReservedBlockingIP(ip));

    const ipv4Results = results.filter((ip) => iptool.isV4Format(ip))

    if(ipv4Results.length > 0) {
      return ipv4Results[0]; // return ipv4 address as a priority
    }

    if(results.length > 0) {
      log.info(`Domain ${domain} ======> ${results[0]}`);
      return results[0];
    }

    return null;
  }

  async updateDomainCache(domain) {
    return domainBlock.resolveDomain(domain);
  }

  getAllDomains() {
    const mappings = this.getDomainMapping();
    if(mappings) {
      const values = Object.values(mappings);
      const domains = [];
      values.forEach((value) => {
        if(value) {
          domains.push(...Object.keys(value));
        }
      });
      return domains;
    } else {
      return [];
    }
  }

  async updateAllDomains() {
    log.info("Updating all domains...");
    return Promise.all(this.getAllDomains().map(async domain => this.updateDomainCache(domain)));
  }

  // redirect targetDomain to the ip address of safe domain
  async generateConfig(macAddress, safeDomain, targetDomains) {
    const ip = await this.loadDomainCache(safeDomain);
    if(ip) {
      return targetDomains.map((targetDomain) => {
        return this.getDNSMasqEntry(macAddress, ip, targetDomain);
      });
    } else {
      return [];
    }
  }

  async applySafeSearch() {
    await this.applySystemSafeSearch();

    for(const macAddress in this.enabledMacAddresses) {
      await this.applyDeviceSafeSearch(macAddress)
    }
  }

  async applySystemSafeSearch() {
    if(this.systemSwitch && this.adminSystemSwitch) {
      const config = await this.getSafeSearchConfig();
      return this.systemStart(config);
    } else {
      return this.systemStop();
    }
  }

  async applyDeviceSafeSearch(macAddress) {
    try {
      if(this.enabledMacAddresses[macAddress]) {
        const config = await this.getSafeSearchConfig();
        return this.perDeviceStart(macAddress, config)
      } else {
        return this.perDeviceStop(macAddress);
      }
    } catch(err) {
      log.error(`Failed to apply safe search on device ${macAddress}, err: ${err}`);
    }
  }

  async generateConfigFile(macAddress, config) {
    let entries = [];

    for(const type in config) {
      const value = config[type];
      if(value === 'off') {
        continue;
      }

      const key = this.getMappingKey(type, value);
      const result = this.getMappingResult(key);
      if(result) {
        const safeDomains = Object.keys(result);
        await Promise.all(safeDomains.map(async (safeDomain) => {
          const targetDomains = result[safeDomain];
          const configs = await this.generateConfig(macAddress, safeDomain, targetDomains);
          entries.push(...configs);
        }));
      }
    }

    entries.push("");

    //await fs.writeFileAsync(destinationFile, entries.join("\n"));
    return entries.join("\n");
  }

  async saveConfigFile(file, content) {
    return fs.writeFileAsync(file, content);
  }

  async loadConfigFile(file) {
    return fs.readFileAsync(file, {encoding: 'utf8'});
  }

  async deleteConfigFile(destinationFile) {
    return fs.unlinkAsync(destinationFile).catch(() => undefined);
  }

  // system level

  getConfigFile(macAddress) {
    if(macAddress) {
      return `${devicemasqConfigFolder}/safeSearch_${macAddress}.conf`;
    } else {
      return safeSearchConfigFile;
    }
  }

  async systemStart(config) {
    const configString = await this.generateConfigFile(undefined, config);
    const existingString = await this.loadConfigFile(this.getConfigFile()).catch((err) => null);
    if(configString !== existingString || existingString === null) {
      await this.saveConfigFile(this.getConfigFile(), configString);
      await dnsmasq.start(true);
    }
    return;
  }

  async systemStop() {
    await this.deleteConfigFile(safeSearchConfigFile);
    await dnsmasq.start(true);
  }

  /*
   * Safe Search DNS server will use local primary dns server as upstream server
   */
  async startDeviceMasq() {
    if(this.starting) {
      return;
    }

    try {
      this.starting = true;
      await this.delay(5000);
      this.starting = false;
      return exec("sudo systemctl restart devicemasq");
    } catch(err) {
      log.error(`Failed to restart devicemasq, err: ${err}`);
    }
  }

  async stopDeviceMasq() {
    return exec("sudo systemctl stop devicemasq");
  }

  async isDeviceMasqRunning() {
    try {
      await exec("systemctl is-active devicemasq");
    } catch(err) {
      return false;
    }

    return true;
  }

  async perDeviceStart(mac, config) {
    const configString = await this.generateConfigFile(mac, config);
    const existingString = await this.loadConfigFile(this.getConfigFile(mac)).catch((err) => null);
    if(configString !== existingString || existingString === null) {
      await this.saveConfigFile(this.getConfigFile(mac), configString);
      await this.startDeviceMasq();
    }
    await this.delay(8 * 1000); // wait for a while before activating the dns redirect
    await exec(`sudo ipset -! add devicedns_mac_set ${mac}`);
  }

  async perDeviceStop(mac) {
    await exec(`sudo ipset -! del devicedns_mac_set ${mac}`);
    const file = this.getConfigFile(mac);
    await this.deleteConfigFile(file);
    await this.startDeviceMasq();
  }

  // global on/off
  async globalOn() {
    await this.addIptablesRules();
    this.adminSystemSwitch = true;
    await this.applySystemSafeSearch();
  }

  async globalOff() {
    await this.removeIptablesRules();
    this.adminSystemSwitch = false;
    await this.applySystemSafeSearch();
  }

  async addIptablesRules() {
    const localIP = sysManager.myIp();
    const deviceDNS = `${localIP}:8863`;

    const ipv6s = sysManager.myIp6();

    for(const protocol of ["tcp", "udp"]) {
      const deviceDNSRule = `sudo iptables -w -t nat -I PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS}`;
      const cmd = iptables.wrapIptables(deviceDNSRule);
      await exec(cmd).catch(() => undefined);

      for(const ip6 of ipv6s) {
        if (ip6.startsWith("fe80::")) {
          // use local link ipv6 for port forwarding, both ipv4 and v6 dns traffic should go through dnsmasq

          const deviceDNS6 = `[${ip6}]:8863`;

          const deviceDNSRule = `sudo ip6tables -w -t nat -I PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS6}`;
          const cmd = iptables.wrapIptables(deviceDNSRule);
          await exec(cmd).catch(() => undefined);
        }
      }
    }
  }

  async removeIptablesRules() {
    const localIP = sysManager.myIp();
    const deviceDNS = `${localIP}:8863`;

    const ipv6s = sysManager.myIp6();

    for(const protocol of ["tcp", "udp"]) {
      const deviceDNSRule = `sudo iptables -w -t nat -D PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS}`;
      const cmd = iptables.wrapIptables(deviceDNSRule);
      await exec(cmd).catch(() => undefined);

      for(const ip6 of ipv6s) {
        if (ip6.startsWith("fe80::")) {
          // use local link ipv6 for port forwarding, both ipv4 and v6 dns traffic should go through dnsmasq

          const deviceDNS6 = `[${ip6}]:8863`;

          const deviceDNSRule = `sudo ip6tables -w -t nat -D PREROUTING -p ${protocol} -m set --match-set devicedns_mac_set src --dport 53 -j DNAT --to-destination ${deviceDNS6}`;
          const cmd = iptables.wrapIptables(deviceDNSRule);
          await exec(cmd).catch(() => undefined);
        }
      }
    }
  }
}

module.exports = SafeSearchPlugin
<|endoftext|>


<commit_before>/*
	Class: UI.Container
		The UI.Container class defines objects manage the content of the container
		that manage containers use by several object like windows, menus.
*/

/**
 * [initialize description]
 * @class  UI.Container
 * @extends {UI.Component}
 * @author Jerome Vial
 */
UI.Container = new Class({

	Extends: UI.Component,

	Implements: [UI.Display],

	name: 'container',

	options: {
		name: 'container',

		node: null,

		tag: 'div',
		/*resizable: false,
		resizeBorders: ['top','right','bottom','left']*/
	},

	/**
	 * [initialize description]
	 * @param  {[type]} options [description]
	 * @return {[type]}         [description]
	 */
	initialize: function(options){
		this.parent(options);

		if (this.options.comp) {
			this._initComp(this.options.comp);
		} else {
			this._initComponent();
		}

		return this;
	},

	/*
	Method: _initElement
		private function

		Creates html structure and inject it to the dom. The container is _initElement with two elements: the wrapper and the content.
		If the option scroll is set to true, it will also add the scrollbar object

	Returns:
		(void)

	See also:
		<UI.Component::_initElement>
	*/
	_initElement: function() {
		this.parent();

		var opts = this.options;
		this.menu = {};

		if ( opts.head ) this._initHead( opts.head );
		if ( opts.menu ) this.setMenu( opts.menu );
		if ( this.name == 'window' ) this._initBody();
		if ( opts.useOverlay ) this._initOverlay();

		if ( opts.foot ) this._initFoot( opts.foot );

		if (this.options.useUnderlay)
			this._initUnderlay();
	},

	/*
		function : _initContainer

			Build the split containers

	*/
	_initComponent: function() {
		var self = this,
			opts = this.options;

		if (opts.node === null) return;

		_log('_initComponent', opts.node);

		this.node = [];

		if (typeOf(opts.node) == 'array') {
			for (var i = 0; i < opts.node.length; i++) {
				this.addComponent(opts.node[i]);
			}
		} else if (typeOf(opts.node) == 'object') {
			var node = opts.node;

			this.addComponent(node);
		}

	},

	/*
	Method: _initComp
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	/**
	 * Initialize internal container components
	 * @param  {Mixin} comp Compenent description
	 * @return {[type]}      [description]
	 */
	_initComp: function(comp) {
		//_log('_initComp', comp);
		var self = this;

		if (typeOf(comp) == 'string' ) {
			this.addComp(comp);
		} else if (typeOf(comp) == 'object' ){

		} else if (typeOf(comp) == 'array' ) {
			comp.each(function(name) {
				self.addComp(name);
			});
		}
	},

	/**
	 * [_initComp description]
	 * @param  {[type]} name [description]
	 * @return {[type]}      [description]
	 */
	addComp: function(name, position, element) {
		//_log('addComp', name, position, element);
		var self = this;
		position = position || 'bottom';
		element = element || this.element;

		//_log('_addComp', name);

		if (!element) {
			_warn('Container is ', element);
			return;
		}

		var comp = this[name] = new Element('div')
			.addClass('container-'+name)
			.inject(element, position);

		return comp;
		/*this.addEvents({
			resize: function() {
				//_log('resize from head', this, this.head.getSize().y+'px');
				this.element.setStyle('padding-top', this.head.getSize().y+'px');
			}
		});*/
	},

	/*
	Method: _initClass
		private function

		_initClass container related class

	Returns:
		(void)
	*/
	_initClass: function(){
		this.parent();

		this.element.addClass('ui-container');
	},

	/*
	Method: _initHead
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	_initHead: function() {
		var self = this;

		this.head = new Element('div')
			.addClass('container-head')
			.inject(this.element,'top')
			.addEvent('dblclick', function() {
				self.fireEvent('max');
			});
	},

	/*
	Function: setTitle
		set title html

	Arguments:
		html - (string) html formatted title

	Returns:
		this
	*/
	setTitle: function(title){
		if (this.title && this.head)
			return this.title.set('text', title);
	},

	/*
	Method: setMenu
		inject a menu in da head

	Returns:
		(void)
	 */
	setMenu: function(opts) {
		var self = this,
			comp = opts.comp || 'head';

		if (!this[comp])
			this['_init'+comp.capitalize()]();

		var container = this[comp].getElement('.'+comp+'-menu');

		if (!container) {
			container = new Element('div', {
				'class': comp+'-menu'
			}).inject(this[comp]);
		}

		//_log('_init'+comp.capitalize());
		var menu = new UI.Menu(opts);

		this.menu[opts.name] = menu;

		// _log('setMenu', opts.name, menu);

		menu.addEvents({
			'toggle': function() {
				self.fireEvent('resize');
			},
			'change': function(value) {
				self.fireEvent('menuChange', value);
			}
		}).inject(container);

		this.addEvents({
			onMinimize: function() {
				menu.hide();
			},
			onNormalize: function() {
				menu.show();
			}
		});

		//  need to find an event driven solution
		this.element.setStyle('padding-top', this[comp].getSize().y+'px');

		return menu;
	},

	/*
	Method: _initFoot
		private function

		inject a container, add it to the bottom

	Returns:
		(void)
	 */
	_initFoot: function(options) {
		var self = this;

		this.foot = new Element('div', {
			'class': 'container-foot'
		}).inject(this.element, 'bottom');
	},

	/*
	Method: _initStatus
		private function

		inject a container, add it to the bottom

	Returns:
		(void)
	 */
	_initStatus: function(component, context) {
		var self = this;

		component = component || 'foot';

		if (!this[component])
			this['_init'+component.capitalize()]();

		this.status = new Element('div', {
			'class': 'container-status'
		}).inject(this[component]);
	},

	/*
	Method: _initOverlay
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	_initOverlay: function() {
		var self = this;

		this.overlay = new Element('div', {
			'class': 'container-overlay'
		}).inject(this.element);

		this.addEvent('onLoadComplete', function() {
			this.overlay.hide();
		});

		this.overlay.hide();

		this.addEvents({
			onBlur: function() {
				//_log('blur');
				self.overlay.show();
			},
			onDragComplete: function() {
				//_log('darg com', ui.window.underlay);
				self.overlay.hide();
			},
			onDragStart: function(){
				_log('darg start', this);
				self.overlay.show();
			},
			onResizeComplete: function() {
				self.overlay.hide();
				this.coord = this.element.getCoordinates();
			},
			onResizeStart: function() {
				self.overlay.show();
			}
		});
	},

	_initUnderlay: function() {
		//_log('_initUnderlay', this.device);
		var self = this;

		this.underlay = new Element('div', {
			'class': 'dialog-underlay',
			styles: {
				zIndex: 10,
				//display: 'none'
			}
		}).inject(this.element, 'before');

		this.underlay.addEvent('click', function() {
			self.minimize();
		});

 		this.addEvent('close', function(){
			self.underlay.destroy();
		});
   	},

	focus: function(){
		//_log('focus');
		//this.fireEvent('focus');
	}
});
<commit_msg>remove logs<commit_after>/*
	Class: UI.Container
		The UI.Container class defines objects manage the content of the container
		that manage containers use by several object like windows, menus.
*/

/**
 * [initialize description]
 * @class  UI.Container
 * @extends {UI.Component}
 * @author Jerome Vial
 */
UI.Container = new Class({

	Extends: UI.Component,

	Implements: [UI.Display],

	name: 'container',

	options: {
		name: 'container',

		node: null,

		tag: 'div',
		/*resizable: false,
		resizeBorders: ['top','right','bottom','left']*/
	},

	/**
	 * [initialize description]
	 * @param  {[type]} options [description]
	 * @return {[type]}         [description]
	 */
	initialize: function(options){
		this.parent(options);

		if (this.options.comp) {
			this._initComp(this.options.comp);
		} else {
			this._initComponent();
		}

		return this;
	},

	/*
	Method: _initElement
		private function

		Creates html structure and inject it to the dom. The container is _initElement with two elements: the wrapper and the content.
		If the option scroll is set to true, it will also add the scrollbar object

	Returns:
		(void)

	See also:
		<UI.Component::_initElement>
	*/
	_initElement: function() {
		this.parent();

		var opts = this.options;
		this.menu = {};

		if ( opts.head ) this._initHead( opts.head );
		if ( opts.menu ) this.setMenu( opts.menu );
		if ( this.name == 'window' ) this._initBody();
		if ( opts.useOverlay ) this._initOverlay();

		if ( opts.foot ) this._initFoot( opts.foot );

		if (this.options.useUnderlay)
			this._initUnderlay();
	},

	/*
		function : _initContainer

			Build the split containers

	*/
	_initComponent: function() {
		var self = this,
			opts = this.options;

		if (opts.node === null) return;

		//_log('_initComponent', opts.node);

		this.node = [];

		if (typeOf(opts.node) == 'array') {
			for (var i = 0; i < opts.node.length; i++) {
				this.addComponent(opts.node[i]);
			}
		} else if (typeOf(opts.node) == 'object') {
			var node = opts.node;

			this.addComponent(node);
		}

	},

	/*
	Method: _initComp
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	/**
	 * Initialize internal container components
	 * @param  {Mixin} comp Compenent description
	 * @return {[type]}      [description]
	 */
	_initComp: function(comp) {
		//_log('_initComp', comp);
		var self = this;

		if (typeOf(comp) == 'string' ) {
			this.addComp(comp);
		} else if (typeOf(comp) == 'object' ){

		} else if (typeOf(comp) == 'array' ) {
			comp.each(function(name) {
				self.addComp(name);
			});
		}
	},

	/**
	 * [_initComp description]
	 * @param  {[type]} name [description]
	 * @return {[type]}      [description]
	 */
	addComp: function(name, position, element) {
		//_log('addComp', name, position, element);
		var self = this;
		position = position || 'bottom';
		element = element || this.element;

		//_log('_addComp', name);

		if (!element) {
			_warn('Container is ', element);
			return;
		}

		var comp = this[name] = new Element('div')
			.addClass('container-'+name)
			.inject(element, position);

		return comp;
		/*this.addEvents({
			resize: function() {
				//_log('resize from head', this, this.head.getSize().y+'px');
				this.element.setStyle('padding-top', this.head.getSize().y+'px');
			}
		});*/
	},

	/*
	Method: _initClass
		private function

		_initClass container related class

	Returns:
		(void)
	*/
	_initClass: function(){
		this.parent();

		this.element.addClass('ui-container');
	},

	/*
	Method: _initHead
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	_initHead: function() {
		var self = this;

		this.head = new Element('div')
			.addClass('container-head')
			.inject(this.element,'top')
			.addEvent('dblclick', function() {
				self.fireEvent('max');
			});
	},

	/*
	Function: setTitle
		set title html

	Arguments:
		html - (string) html formatted title

	Returns:
		this
	*/
	setTitle: function(title){
		if (this.title && this.head)
			return this.title.set('text', title);
	},

	/*
	Method: setMenu
		inject a menu in da head

	Returns:
		(void)
	 */
	setMenu: function(opts) {
		var self = this,
			comp = opts.comp || 'head';

		if (!this[comp])
			this['_init'+comp.capitalize()]();

		var container = this[comp].getElement('.'+comp+'-menu');

		if (!container) {
			container = new Element('div', {
				'class': comp+'-menu'
			}).inject(this[comp]);
		}

		//_log('_init'+comp.capitalize());
		var menu = new UI.Menu(opts);

		this.menu[opts.name] = menu;

		// _log('setMenu', opts.name, menu);

		menu.addEvents({
			'toggle': function() {
				self.fireEvent('resize');
			},
			'change': function(value) {
				self.fireEvent('menuChange', value);
			}
		}).inject(container);

		this.addEvents({
			onMinimize: function() {
				menu.hide();
			},
			onNormalize: function() {
				menu.show();
			}
		});

		//  need to find an event driven solution
		this.element.setStyle('padding-top', this[comp].getSize().y+'px');

		return menu;
	},

	/*
	Method: _initFoot
		private function

		inject a container, add it to the bottom

	Returns:
		(void)
	 */
	_initFoot: function(options) {
		var self = this;

		this.foot = new Element('div', {
			'class': 'container-foot'
		}).inject(this.element, 'bottom');
	},

	/*
	Method: _initStatus
		private function

		inject a container, add it to the bottom

	Returns:
		(void)
	 */
	_initStatus: function(component, context) {
		var self = this;

		component = component || 'foot';

		if (!this[component])
			this['_init'+component.capitalize()]();

		this.status = new Element('div', {
			'class': 'container-status'
		}).inject(this[component]);
	},

	/*
	Method: _initOverlay
		private function

		create an overlay displayed when container is disabled (when moved or resized)

	Returns:
		(void)
	 */
	_initOverlay: function() {
		var self = this;

		this.overlay = new Element('div', {
			'class': 'container-overlay'
		}).inject(this.element);

		this.addEvent('onLoadComplete', function() {
			this.overlay.hide();
		});

		this.overlay.hide();

		this.addEvents({
			onBlur: function() {
				//_log('blur');
				self.overlay.show();
			},
			onDragComplete: function() {
				//_log('darg com', ui.window.underlay);
				self.overlay.hide();
			},
			onDragStart: function(){
				_log('darg start', this);
				self.overlay.show();
			},
			onResizeComplete: function() {
				self.overlay.hide();
				this.coord = this.element.getCoordinates();
			},
			onResizeStart: function() {
				self.overlay.show();
			}
		});
	},

	_initUnderlay: function() {
		//_log('_initUnderlay', this.device);
		var self = this;

		this.underlay = new Element('div', {
			'class': 'dialog-underlay',
			styles: {
				zIndex: 10,
				//display: 'none'
			}
		}).inject(this.element, 'before');

		this.underlay.addEvent('click', function() {
			self.minimize();
		});

 		this.addEvent('close', function(){
			self.underlay.destroy();
		});
   	},

	focus: function(){
		//_log('focus');
		//this.fireEvent('focus');
	}
});
<|endoftext|>


<commit_before>import React from 'react';
import { PrismCode } from 'react-prism';
import { Button, Container, Row, Col, Jumbotron } from 'reactstrap';
import { Link } from 'react-router';
import Example from '../examples/import-basic';

const importBasic = require('!!raw!../examples/import-basic');

export default () => {
  return (

      <Jumbotron tag="section" className="jumbotron-header text-center mb-3">
        <Container>
          <Row>
            <Col>

reactstrap

                Easy to use React Bootstrap 4 components

                <Button outline color="danger" href="https://github.com/reactstrap/reactstrap">View on Github</Button>
                <Button color="danger" tag={Link} to="/components/">View Components</Button>

            </Col>
          </Row>
        </Container>
      </Jumbotron>
      <Container>
        <Row className="justify-content-sm-center">
          <Col sm={8}>

Installation

NPM

Install reactstrap and peer dependencies via NPM

              <PrismCode className="language-bash">npm install --save reactstrap@next react react-dom</PrismCode>

Import the components you need

              <Example />

              <PrismCode className="language-jsx">
                {importBasic}
              </PrismCode>

Getting Started with Create React App

Follow the create-react-app instructions up to the Adding Bootstrap section and instead follow the reactstrap version of adding bootstrap.

tl;dr

              <PrismCode className="language-bash">
{`npm install -g create-react-app

create-react-app my-app
cd my-app/
npm start`}
              </PrismCode>

              Then open http://localhost:3000/ to see your app. The initial structure of your app is setup. Next, let's add reactstrap and bootstrap.

Adding Bootstrap

Install reactstrap and Bootstrap from NPM. Reactstrap does not include Bootstrap CSS so this needs to be installed as well:

              <PrismCode className="language-bash">
  {`npm install bootstrap --save
npm install --save reactstrap@next react react-dom`}
              </PrismCode>

Import Bootstrap CSS in the src/index.js file:

              <PrismCode className="language-bash">import 'bootstrap/dist/css/bootstrap.css';</PrismCode>

Import required reactstrap components within src/App.js file or your custom component files:

              <PrismCode className="language-bash">
                {`import { Button } from 'reactstrap';`}
              </PrismCode>

Now you are ready to use the imported reactstrap components within your component hierarchy defined in the render method. Here is an example App.js redone using reactstrap.

CDN

Reactstrap can be included directly in your application's bundle or excluded during compilation and linked directly to a CDN.

              <PrismCode className="language-jsx">
                https://cdnjs.cloudflare.com/ajax/libs/reactstrap/4.8.0/reactstrap.min.js
              </PrismCode>

                Note: When using the external CDN library, be sure to include the required dependencies as necessary prior to the Reactstrap library:

React

ReactTransitionGroup

Check out the demo here

About the Project

This library contains React Bootstrap 4 components that favor composition and control. The library does not depend on jQuery or Bootstrap javascript. However, https://popper.js.org/ via https://github.com/souporserious/react-popper is relied upon for advanced positioning of content like Tooltips, Popovers, and auto-flipping Dropdowns.

There are a few core concepts to understand in order to make the most out of this library.

1) Your content is expected to be composed via props.children rather than using named props to pass in Components.

              <PrismCode className="language-jsx">
{`// Content passed in via props
const Example = (props) => {
  return (

This is a tooltip <TooltipTrigger tooltip={TooltipContent}>example</TooltipTrigger>!

  );
}

// Content passed in as children (Preferred)
const PreferredExample = (props) => {
  return (

      This is a tooltip example.
      <Tooltip target="TooltipExample">
        <TooltipContent/>
      </Tooltip>

  );
}`}
              </PrismCode>

              2) Attributes in this library are used to pass in state, conveniently apply modifier classes, enable advanced functionality (like popperjs), or automatically include non-content based elements.

Examples:

isOpen - current state for items like dropdown, popover, tooltip

toggle - callback for toggling isOpen in the controlling component

color - applies color classes, ex: {'<Button color="danger"/>'}

size for controlling size classes. ex: {'<Button size="sm"/>'}

tag - customize component output by passing in an element name or Component

boolean based props (attributes) when possible for alternative style classes or sr-only content

          </Col>
        </Row>
      </Container>

  );
};
<commit_msg>docs(install): Use the minified version of Bootstrap CSS (#900)<commit_after>import React from 'react';
import { PrismCode } from 'react-prism';
import { Button, Container, Row, Col, Jumbotron } from 'reactstrap';
import { Link } from 'react-router';
import Example from '../examples/import-basic';

const importBasic = require('!!raw!../examples/import-basic');

export default () => {
  return (

      <Jumbotron tag="section" className="jumbotron-header text-center mb-3">
        <Container>
          <Row>
            <Col>

reactstrap

                Easy to use React Bootstrap 4 components

                <Button outline color="danger" href="https://github.com/reactstrap/reactstrap">View on Github</Button>
                <Button color="danger" tag={Link} to="/components/">View Components</Button>

            </Col>
          </Row>
        </Container>
      </Jumbotron>
      <Container>
        <Row className="justify-content-sm-center">
          <Col sm={8}>

Installation

NPM

Install reactstrap and peer dependencies via NPM

              <PrismCode className="language-bash">npm install --save reactstrap@next react react-dom</PrismCode>

Import the components you need

              <Example />

              <PrismCode className="language-jsx">
                {importBasic}
              </PrismCode>

Getting Started with Create React App

Follow the create-react-app instructions up to the Adding Bootstrap section and instead follow the reactstrap version of adding bootstrap.

tl;dr

              <PrismCode className="language-bash">
{`npm install -g create-react-app

create-react-app my-app
cd my-app/
npm start`}
              </PrismCode>

              Then open http://localhost:3000/ to see your app. The initial structure of your app is setup. Next, let's add reactstrap and bootstrap.

Adding Bootstrap

Install reactstrap and Bootstrap from NPM. Reactstrap does not include Bootstrap CSS so this needs to be installed as well:

              <PrismCode className="language-bash">
  {`npm install bootstrap --save
npm install --save reactstrap@next react react-dom`}
              </PrismCode>

Import Bootstrap CSS in the src/index.js file:

              <PrismCode className="language-bash">import 'bootstrap/dist/css/bootstrap.min.css';</PrismCode>

Import required reactstrap components within src/App.js file or your custom component files:

              <PrismCode className="language-bash">
                {`import { Button } from 'reactstrap';`}
              </PrismCode>

Now you are ready to use the imported reactstrap components within your component hierarchy defined in the render method. Here is an example App.js redone using reactstrap.

CDN

Reactstrap can be included directly in your application's bundle or excluded during compilation and linked directly to a CDN.

              <PrismCode className="language-jsx">
                https://cdnjs.cloudflare.com/ajax/libs/reactstrap/4.8.0/reactstrap.min.js
              </PrismCode>

                Note: When using the external CDN library, be sure to include the required dependencies as necessary prior to the Reactstrap library:

React

ReactTransitionGroup

Check out the demo here

About the Project

This library contains React Bootstrap 4 components that favor composition and control. The library does not depend on jQuery or Bootstrap javascript. However, https://popper.js.org/ via https://github.com/souporserious/react-popper is relied upon for advanced positioning of content like Tooltips, Popovers, and auto-flipping Dropdowns.

There are a few core concepts to understand in order to make the most out of this library.

1) Your content is expected to be composed via props.children rather than using named props to pass in Components.

              <PrismCode className="language-jsx">
{`// Content passed in via props
const Example = (props) => {
  return (

This is a tooltip <TooltipTrigger tooltip={TooltipContent}>example</TooltipTrigger>!

  );
}

// Content passed in as children (Preferred)
const PreferredExample = (props) => {
  return (

      This is a tooltip example.
      <Tooltip target="TooltipExample">
        <TooltipContent/>
      </Tooltip>

  );
}`}
              </PrismCode>

              2) Attributes in this library are used to pass in state, conveniently apply modifier classes, enable advanced functionality (like popperjs), or automatically include non-content based elements.

Examples:

isOpen - current state for items like dropdown, popover, tooltip

toggle - callback for toggling isOpen in the controlling component

color - applies color classes, ex: {'<Button color="danger"/>'}

size for controlling size classes. ex: {'<Button size="sm"/>'}

tag - customize component output by passing in an element name or Component

boolean based props (attributes) when possible for alternative style classes or sr-only content

          </Col>
        </Row>
      </Container>

  );
};
<|endoftext|>


<commit_before>// Find index at end of param string (not at start)
String.prototype.indexOfEnd = function(string) {
  var io = this.indexOf(string);
  return io == -1 ? -1 : io + string.length;
}

var
  db = require('mime-db'),
  path = require('path'),
  fs = require('fs');

var functions = {
  generateDataOosUtilValues: function(){
    console.log('*** generateDataOosUtilValues ***');

    var
      sqlStmt = '',
      temp,
      extensions = {}
    ;

    var deleteElements = function(pSearch, pArray){
      //Make sure that at least one element exist for each ext
      for (var i = 0; i < pArray.length && pArray.length > 1; i++){
        // Remove application specific mime types
        if (pArray[i].indexOf(pSearch) >= 0){
          pArray.splice(i, 1);
          i += -1; //Since splice removes element from array need to adjust pointer
        }
      }
    }//deleteElements

    // db has the mime-type with an array of extensions
    // Reverse this to have extensions with array of mime-types
    console.log('Reversing mime-types and extensions');
    for (var key in db){
      if (db[key].extensions){
        var exts = db[key].extensions;
        for(var i = 0; i < exts.length; i++){
          //Check if it array already exists and if it's unique
          if(extensions[exts[i]] && extensions[exts[i]].indexOf(key) < 0){
            extensions[exts[i]].push(key);
          }
          else{
            extensions[exts[i]] = [key];
          }
        }//for i
      }
    }//for key

    // Some extensions will have multiple mime-types.
    // Adjust down to one per extension
    console.log('Reducing to one mime-type per extension');
    for (var ext in extensions){

      //Take several passes to get down to one mime-type per extension
      var j = 0;
      while (extensions[ext].length > 1){
        switch(j){
          case 0:
            //First pass, remove non-standard mime types
            deleteElements('/x-', extensions[ext]);
            break;
          case 1:
            deleteElements('application/', extensions[ext]);
          case 2:
            deleteElements('audio/wave', extensions[ext]);
          case 3:
            deleteElements('image/', extensions[ext]);
        }//switch

        j++;

        // Error out if we can't filter down
        if (j > 10){
          console.log('Can\'t filter down mime-type');
          console.log(ext, ':', extensions[ext]);
          process.exit(1);
        }
      }//while

    }//for duplicates

    console.log('Creating insert statement');
    //Create insert statement
    sqlStmt  = '-- This file is generated, do not modify.\n\n';
    sqlStmt += 'begin\n  delete oos_util_values;\n';
    sqlStmt += '  insert all\n';
    for(var ext in extensions){
      temp = '  into oos_util_values(cat, name, value) values('mime-type', '%name%','%value%');\n';
      temp = temp.replace(/\%name\%/g, ext);
      temp = temp.replace(/\%value\%/g, extensions[ext][0]);
      sqlStmt += temp;
    }
    sqlStmt += '  select 1 from dual;\n';
    sqlStmt += 'end;\n'
    sqlStmt += '/\n';
    sqlStmt += 'commit;\n';

    fs.writeFileSync(path.resolve(__dirname,'../data/oos_util_values.sql'), sqlStmt);

  },//generateDataOosUtilValues

  appendFile : function (pFile, pContent){
    fs.appendFileSync(path.resolve(__dirname,pFile), pContent + '\n');
  },// appendFile

  readFile : function (pFile){
    return fs.readFileSync(path.resolve(__dirname,pFile),'utf8');
  },// readFile

  writeFile : function (pFile, pContent){
    fs.writeFileSync(path.resolve(__dirname,pFile), pContent);
  },// writeFile

  /**
   * Checks that package files all contain a "/" at EOF so it will compile properly in SQL*Plus
   *
   *
   * Related Tickets:
   *  - #37
   *
   * @param pPackages JSON object of packages (use config.objects.packages)
   */
  validatePackages : function(pPackages){
    var
      contents,
      error = false;

    for (var packageName in pPackages){
      for (var fileExt in pPackages[packageName]){
        contents = this.readFile(pPackages[packageName][fileExt]);

        // #37
        if (contents.search(/\/\n*$/) === -1){
          console.log('File missing "/" at end: ' + pPackages[packageName][fileExt]);
          error = true;
        }
      }// fileExt
    }//packageName

    if (error){
      process.exit(1);
    }

  },//validatePackages

  /**
   * Creates Synonym script
   *
   */
  createSynonymScript : function (pConfig){
    var template = 'create or replace synonym %OBJECT% for &from_user..%OBJECT%;\n';

    var temp = this.readFile(pConfig.files.createSynonyms);

    temp = temp.slice(0,temp.indexOfEnd('-- AUTOREPLACE_START'));
    temp += '\n\n';

    // Don't do tables since tables aren't officially supported or necessary yet
    for (var package in pConfig.objects.packages){
      temp += template.replace(/%OBJECT%/g, package);
    }//package

    this.writeFile(pConfig.files.createSynonyms, temp);

  },//createSynonymScript

  /**
   * Creates Grant script
   *
   */
  createGrantScript : function (pConfig){
    var template = {
      execute : 'grant execute on %OBJECT% to &to_user;\n'
    };

    var temp = this.readFile(pConfig.files.createGrants);

    temp = temp.slice(0,temp.indexOfEnd('-- AUTOREPLACE_START'));
    temp += '\n\n';

    // Don't do tables since tables aren't officially supported or necessary yet
    for (var package in pConfig.objects.packages){
      temp += template.execute.replace(/%OBJECT%/g, package);
    }//package

    this.writeFile(pConfig.files.createGrants, temp);

  }//createGrantScript
};// functions

module.exports = functions;
<commit_msg>#75: fixed quotes<commit_after>// Find index at end of param string (not at start)
String.prototype.indexOfEnd = function(string) {
  var io = this.indexOf(string);
  return io == -1 ? -1 : io + string.length;
}

var
  db = require('mime-db'),
  path = require('path'),
  fs = require('fs');

var functions = {
  generateDataOosUtilValues: function(){
    console.log('*** generateDataOosUtilValues ***');

    var
      sqlStmt = '',
      temp,
      extensions = {}
    ;

    var deleteElements = function(pSearch, pArray){
      //Make sure that at least one element exist for each ext
      for (var i = 0; i < pArray.length && pArray.length > 1; i++){
        // Remove application specific mime types
        if (pArray[i].indexOf(pSearch) >= 0){
          pArray.splice(i, 1);
          i += -1; //Since splice removes element from array need to adjust pointer
        }
      }
    }//deleteElements

    // db has the mime-type with an array of extensions
    // Reverse this to have extensions with array of mime-types
    console.log('Reversing mime-types and extensions');
    for (var key in db){
      if (db[key].extensions){
        var exts = db[key].extensions;
        for(var i = 0; i < exts.length; i++){
          //Check if it array already exists and if it's unique
          if(extensions[exts[i]] && extensions[exts[i]].indexOf(key) < 0){
            extensions[exts[i]].push(key);
          }
          else{
            extensions[exts[i]] = [key];
          }
        }//for i
      }
    }//for key

    // Some extensions will have multiple mime-types.
    // Adjust down to one per extension
    console.log('Reducing to one mime-type per extension');
    for (var ext in extensions){

      //Take several passes to get down to one mime-type per extension
      var j = 0;
      while (extensions[ext].length > 1){
        switch(j){
          case 0:
            //First pass, remove non-standard mime types
            deleteElements('/x-', extensions[ext]);
            break;
          case 1:
            deleteElements('application/', extensions[ext]);
          case 2:
            deleteElements('audio/wave', extensions[ext]);
          case 3:
            deleteElements('image/', extensions[ext]);
        }//switch

        j++;

        // Error out if we can't filter down
        if (j > 10){
          console.log('Can\'t filter down mime-type');
          console.log(ext, ':', extensions[ext]);
          process.exit(1);
        }
      }//while

    }//for duplicates

    console.log('Creating insert statement');
    //Create insert statement
    sqlStmt  = '-- This file is generated, do not modify.\n\n';
    sqlStmt += 'begin\n  delete oos_util_values;\n';
    sqlStmt += '  insert all\n';
    for(var ext in extensions){
      temp = "  into oos_util_values(cat, name, value) values('mime-type', '%name%','%value%');\n";
      temp = temp.replace(/\%name\%/g, ext);
      temp = temp.replace(/\%value\%/g, extensions[ext][0]);
      sqlStmt += temp;
    }
    sqlStmt += '  select 1 from dual;\n';
    sqlStmt += 'end;\n'
    sqlStmt += '/\n';
    sqlStmt += 'commit;\n';

    fs.writeFileSync(path.resolve(__dirname,'../data/oos_util_values.sql'), sqlStmt);

  },//generateDataOosUtilValues

  appendFile : function (pFile, pContent){
    fs.appendFileSync(path.resolve(__dirname,pFile), pContent + '\n');
  },// appendFile

  readFile : function (pFile){
    return fs.readFileSync(path.resolve(__dirname,pFile),'utf8');
  },// readFile

  writeFile : function (pFile, pContent){
    fs.writeFileSync(path.resolve(__dirname,pFile), pContent);
  },// writeFile

  /**
   * Checks that package files all contain a "/" at EOF so it will compile properly in SQL*Plus
   *
   *
   * Related Tickets:
   *  - #37
   *
   * @param pPackages JSON object of packages (use config.objects.packages)
   */
  validatePackages : function(pPackages){
    var
      contents,
      error = false;

    for (var packageName in pPackages){
      for (var fileExt in pPackages[packageName]){
        contents = this.readFile(pPackages[packageName][fileExt]);

        // #37
        if (contents.search(/\/\n*$/) === -1){
          console.log('File missing "/" at end: ' + pPackages[packageName][fileExt]);
          error = true;
        }
      }// fileExt
    }//packageName

    if (error){
      process.exit(1);
    }

  },//validatePackages

  /**
   * Creates Synonym script
   *
   */
  createSynonymScript : function (pConfig){
    var template = 'create or replace synonym %OBJECT% for &from_user..%OBJECT%;\n';

    var temp = this.readFile(pConfig.files.createSynonyms);

    temp = temp.slice(0,temp.indexOfEnd('-- AUTOREPLACE_START'));
    temp += '\n\n';

    // Don't do tables since tables aren't officially supported or necessary yet
    for (var package in pConfig.objects.packages){
      temp += template.replace(/%OBJECT%/g, package);
    }//package

    this.writeFile(pConfig.files.createSynonyms, temp);

  },//createSynonymScript

  /**
   * Creates Grant script
   *
   */
  createGrantScript : function (pConfig){
    var template = {
      execute : 'grant execute on %OBJECT% to &to_user;\n'
    };

    var temp = this.readFile(pConfig.files.createGrants);

    temp = temp.slice(0,temp.indexOfEnd('-- AUTOREPLACE_START'));
    temp += '\n\n';

    // Don't do tables since tables aren't officially supported or necessary yet
    for (var package in pConfig.objects.packages){
      temp += template.execute.replace(/%OBJECT%/g, package);
    }//package

    this.writeFile(pConfig.files.createGrants, temp);

  }//createGrantScript
};// functions

module.exports = functions;
<|endoftext|>


<commit_before>/* This file is part of Indico.
 * Copyright (C) 2002 - 2017 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Indico 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

/* global ChooseUsersPopup:false */

(function($) {
    'use strict';

    var permissionClasses = {
        access: 'accept',
        timetable: 'highlight',
        edit: 'danger',
        material: 'warning'
    };

    $.widget('indico.permissionswidget', {
        options: {
            event_id: null
        },

        _update: function() {
            this.$dataField.val(JSON.stringify(this.data));
            this.element.trigger('change');
        },
        _renderRoleCode: function(code, color) {
            return $('<span>', {
                class: 'role-code',
                text: code,
                css: {
                    'border-color': '#' + color,
                    'color': '#' + color
                }
            });
        },
        _renderLabel: function(principal) {
            var $labelBox = $('<div>', {class: 'label-box'});
            var type = principal._type;
            if (type === 'EventRole') {
                var $text = $('<span>', {class: 'text-normal', text: principal.name});
                var $code = this._renderRoleCode(principal.code, principal.color);
                return $labelBox.append($('<span>', {class: 'flexrow f-a-center'}).append($code).append($text));
            } else {
                var iconClass = type === 'Avatar' || type === 'Email' ? 'icon-user' : 'icon-users';
                var text = type === 'Avatar' ? principal.name : principal.id;
                return $labelBox.append($('<span>', {class: 'label-icon text-normal ' + iconClass, text: text}));
            }
        },
        _renderPermissions: function(principal, permissions) {
            var self = this;
            var $permissions = $('<div>', {class: 'permissions-box flexrow f-a-center f-self-stretch'});
            var $permissionsList = $('<ul>').appendTo($permissions);
            permissions.forEach(function(item) {
                $permissionsList.append($('<li>', {class: 'i-label bold ' + permissionClasses[item]}).append(item));
            });

            var $buttonsGroup = $('<div>', {class: 'group flexrow'});
            var $permissionsEditBtn = $('<button>', {
                'type': 'button',
                'class': 'i-button text-color borderless icon-only icon-edit',
                'data-href': build_url(Indico.Urls.EventPermissions, {confId: this.options.event_id}),
                'data-title': $T.gettext('Assign Permissions'),
                'data-ajax-dialog': '',
                'data-params': JSON.stringify({principal: JSON.stringify(principal), permissions: permissions})
            });

            var $entryDeleteBtn = $('<button>', {
                'type': 'button',
                'class': 'i-button text-color borderless icon-only icon-remove',
                'data-principal': JSON.stringify(principal)
            }).on('click', function() {
                var $this = $(this);
                var title = $T.gettext("Delete entry '{0}'".format(principal.name || principal.id));
                var message = $T.gettext("Are you sure you want to permanently delete this entry?");
                confirmPrompt(message, title).then(function() {
                    self._updateItem($this.data('principal'), []);
                });
            });

            $buttonsGroup.append($permissionsEditBtn, $entryDeleteBtn);
            $buttonsGroup.appendTo($permissions);
            return $permissions;
        },
        _renderItem: function(item) {
            var $item = $('<li>', {class: 'flexrow f-a-center'});
            $item.append(this._renderLabel(item[0]));
            $item.append(this._renderPermissions(item[0], item[1]));
            return $item;
        },
        _renderRoleDropdown: function() {
            var self = this;
            self.$roleDropdown.empty();
            var $roleDropdownLink = self.$roleDropdown.siblings('.js-dropdown');
            var eventRoles = self.$roleDropdown.data('event-roles');
            if (eventRoles.length) {
                eventRoles.forEach(function(role) {
                    if (self._findEntryIndex(role) === -1) {
                        self.$roleDropdown.append(self._renderRoleDropdownItem(role));
                    }
                });
                if (!self.$roleDropdown.children().length) {
                    $roleDropdownLink.addClass('disabled').attr('title', $T('All event roles were added'));
                } else {
                    $roleDropdownLink.removeClass('disabled');
                }
            } else {
                $roleDropdownLink.addClass('disabled').attr('title', $T('No roles created in this event'));
            }
        },
        _renderRoleDropdownItem: function(role) {
            var self = this;
            var $roleDropdownItem = $('<li>', {'class': 'role-item js-add-role', 'data-role': JSON.stringify(role) });
            var $code = this._renderRoleCode(role.code, role.color);
            var $text = $('<span>', {text: role.name});
            $roleDropdownItem.append($('<a>').append($code).append($text)).on('click', function() {
                self._addRole($(this).data('role'));
                $('#permissions-add-entry-menu-target').qbubble('hide');
            });
            return $roleDropdownItem;
        },
        _render: function() {
            var self = this;
            this.$permissionsWidgetList.empty();
            this.data.forEach(function(item) {
                self.$permissionsWidgetList.append(self._renderItem(item));
            });
            this._renderRoleDropdown();
        },
        _findEntryIndex: function(principal) {
            return _.findIndex(this.data, function(item) {
                return item[0].identifier === principal.identifier;
            });
        },
        _updateItem: function(principal, newPermissions) {
            var idx = this._findEntryIndex(principal);
            if (newPermissions.length) {
                this.data[idx][1] = newPermissions;
            } else {
                this.data.splice(idx, 1);
            }
            this._update();
            this._render();
        },
        _addItem: function(principal, permissions) {
            if (this._findEntryIndex(principal) === -1) {
                this.data.push([principal, permissions]);
                this._update();
                this._render();
            } else {
                var text = $T("The User / Group '{0}' is already present as an entry.".format(principal.name));
                $('#flashed-messages').append(
                    $('<div>', {class: 'error-message-box'}).append(
                        $('<div>', {class: 'message-text', text: text})
                    )
                );
            }
        },
        _addUserGroup: function() {
            var self = this;
            function _addPrincipals(principals) {
                $('#flashed-messages').empty();
                principals.forEach(function(principal) {
                    // Grant 'access' permissions when a user/group is added for the first time.
                    self._addItem(principal, ['access']);
                });
            }

            var dialog = new ChooseUsersPopup(
                $T("Select a user or group to add"),
                true, null, true, true, null, false, false, false, _addPrincipals, null, true
            );

            dialog.execute();
        },
        _addRole: function(role) {
            // Grant 'access' permissions when a role is added for the first time.
            this._addItem(role, ['access']);
        },
        _create: function() {
            var self = this;
            this.$permissionsWidgetList = this.element.find('.permissions-widget-list');
            this.$dataField = this.element.find('input[type=hidden]');
            this.$roleDropdown = this.element.find('.entry-role-dropdown');
            this.data = JSON.parse(this.$dataField.val());
            this._render();

            this.element.on('indico:permissionsChanged', function(evt, permissions, principal) {
                self._updateItem(principal, permissions);
            });

            $('.js-add-user-group').on('click', function() {
                self._addUserGroup();
            });

            $('#permissions-add-entry-menu-target').qbubble({
                content: {
                    text: $('#permissions-add-entry-menu')
                },
                style: {
                    classes: 'qtip-allow-overflow'
                },
                position: {
                    my: 'top right',
                    at: 'bottom right'
                }
            });
        }
    });
})(jQuery);
<commit_msg>Show qtip when adding a user/group already added<commit_after>/* This file is part of Indico.
 * Copyright (C) 2002 - 2017 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of the
 * License, or (at your option) any later version.
 *
 * Indico 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

/* global ChooseUsersPopup:false */

(function($) {
    'use strict';

    var permissionClasses = {
        access: 'accept',
        timetable: 'highlight',
        edit: 'danger',
        material: 'warning'
    };

    $.widget('indico.permissionswidget', {
        options: {
            event_id: null
        },

        _update: function() {
            this.$dataField.val(JSON.stringify(this.data));
            this.element.trigger('change');
        },
        _renderRoleCode: function(code, color) {
            return $('<span>', {
                class: 'role-code',
                text: code,
                css: {
                    'border-color': '#' + color,
                    'color': '#' + color
                }
            });
        },
        _renderLabel: function(principal) {
            var $labelBox = $('<div>', {class: 'label-box'});
            var type = principal._type;
            if (type === 'EventRole') {
                var $text = $('<span>', {class: 'text-normal', text: principal.name});
                var $code = this._renderRoleCode(principal.code, principal.color);
                return $labelBox.append($('<span>', {class: 'flexrow f-a-center'}).append($code).append($text));
            } else {
                var iconClass = type === 'Avatar' || type === 'Email' ? 'icon-user' : 'icon-users';
                var text = type === 'Avatar' ? principal.name : principal.id;
                return $labelBox.append($('<span>', {class: 'label-icon text-normal ' + iconClass, text: text}));
            }
        },
        _renderPermissions: function(principal, permissions) {
            var self = this;
            var $permissions = $('<div>', {class: 'permissions-box flexrow f-a-center f-self-stretch'});
            var $permissionsList = $('<ul>').appendTo($permissions);
            permissions.forEach(function(item) {
                $permissionsList.append($('<li>', {class: 'i-label bold ' + permissionClasses[item]}).append(item));
            });

            var $buttonsGroup = $('<div>', {class: 'group flexrow'});
            var $permissionsEditBtn = $('<button>', {
                'type': 'button',
                'class': 'i-button text-color borderless icon-only icon-edit',
                'data-href': build_url(Indico.Urls.EventPermissions, {confId: this.options.event_id}),
                'data-title': $T.gettext('Assign Permissions'),
                'data-ajax-dialog': '',
                'data-params': JSON.stringify({principal: JSON.stringify(principal), permissions: permissions})
            });

            var $entryDeleteBtn = $('<button>', {
                'type': 'button',
                'class': 'i-button text-color borderless icon-only icon-remove',
                'data-principal': JSON.stringify(principal)
            }).on('click', function() {
                var $this = $(this);
                var title = $T.gettext("Delete entry '{0}'".format(principal.name || principal.id));
                var message = $T.gettext("Are you sure you want to permanently delete this entry?");
                confirmPrompt(message, title).then(function() {
                    self._updateItem($this.data('principal'), []);
                });
            });

            $buttonsGroup.append($permissionsEditBtn, $entryDeleteBtn);
            $buttonsGroup.appendTo($permissions);
            return $permissions;
        },
        _renderItem: function(item) {
            var $item = $('<li>', {class: 'flexrow f-a-center'});
            $item.append(this._renderLabel(item[0]));
            $item.append(this._renderPermissions(item[0], item[1]));
            return $item;
        },
        _renderRoleDropdown: function() {
            var self = this;
            self.$roleDropdown.empty();
            var $roleDropdownLink = self.$roleDropdown.siblings('.js-dropdown');
            var eventRoles = self.$roleDropdown.data('event-roles');
            if (eventRoles.length) {
                eventRoles.forEach(function(role) {
                    if (self._findEntryIndex(role) === -1) {
                        self.$roleDropdown.append(self._renderRoleDropdownItem(role));
                    }
                });
                if (!self.$roleDropdown.children().length) {
                    $roleDropdownLink.addClass('disabled').attr('title', $T('All event roles were added'));
                } else {
                    $roleDropdownLink.removeClass('disabled');
                }
            } else {
                $roleDropdownLink.addClass('disabled').attr('title', $T('No roles created in this event'));
            }
        },
        _renderRoleDropdownItem: function(role) {
            var self = this;
            var $roleDropdownItem = $('<li>', {'class': 'role-item js-add-role', 'data-role': JSON.stringify(role) });
            var $code = this._renderRoleCode(role.code, role.color);
            var $text = $('<span>', {text: role.name});
            $roleDropdownItem.append($('<a>').append($code).append($text)).on('click', function() {
                self._addRole($(this).data('role'));
                $('#permissions-add-entry-menu-target').qbubble('hide');
            });
            return $roleDropdownItem;
        },
        _render: function() {
            var self = this;
            this.$permissionsWidgetList.empty();
            // TODO: Sort list properly
            this.data.forEach(function(item) {
                self.$permissionsWidgetList.append(self._renderItem(item));
            });
            this._renderRoleDropdown();
        },
        _findEntryIndex: function(principal) {
            return _.findIndex(this.data, function(item) {
                return item[0].identifier === principal.identifier;
            });
        },
        _updateItem: function(principal, newPermissions) {
            var idx = this._findEntryIndex(principal);
            if (newPermissions.length) {
                this.data[idx][1] = newPermissions;
            } else {
                this.data.splice(idx, 1);
            }
            this._update();
            this._render();
        },
        _addItem: function(principal, permissions) {
            var idx = this._findEntryIndex(principal);
            if (idx === -1) {
                this.data.push([principal, permissions]);
                this._update();
                this._render();
            } else {
                this.$permissionsWidgetList.find('>li').eq(idx).qtip({
                    content: {
                        text: $T('This entry was already added')
                    },
                    show: {
                        ready: true,
                        effect: function() {
                            $(this).fadeIn(300);
                        }
                    },
                    hide: {
                        event: 'unfocused click'
                    },
                    events: {
                        hide: function() {
                            $(this).fadeOut(300);
                            $(this).qtip('destroy');
                        }
                    },
                    position: {
                        my: 'center left',
                        at: 'center right'
                    },
                    style: {
                        classes: 'qtip-danger'
                    }
                });
            }
        },
        _addUserGroup: function() {
            var self = this;
            function _addPrincipals(principals) {
                principals.forEach(function(principal) {
                    // Grant 'access' permissions when a user/group is added for the first time.
                    self._addItem(principal, ['access']);
                });
            }

            var dialog = new ChooseUsersPopup(
                $T("Select a user or group to add"),
                true, null, true, true, null, false, false, false, _addPrincipals, null, true
            );

            dialog.execute();
        },
        _addRole: function(role) {
            // Grant 'access' permissions when a role is added for the first time.
            this._addItem(role, ['access']);
        },
        _create: function() {
            var self = this;
            this.$permissionsWidgetList = this.element.find('.permissions-widget-list');
            this.$dataField = this.element.find('input[type=hidden]');
            this.$roleDropdown = this.element.find('.entry-role-dropdown');
            this.data = JSON.parse(this.$dataField.val());
            this._render();

            this.element.on('indico:permissionsChanged', function(evt, permissions, principal) {
                self._updateItem(principal, permissions);
            });

            $('.js-add-user-group').on('click', function() {
                self._addUserGroup();
            });

            $('#permissions-add-entry-menu-target').qbubble({
                content: {
                    text: $('#permissions-add-entry-menu')
                },
                style: {
                    classes: 'qtip-allow-overflow'
                },
                position: {
                    my: 'top right',
                    at: 'bottom right'
                }
            });
        }
    });
})(jQuery);
<|endoftext|>


<commit_before>(function() {
var ThreeMeshLineMaterialPlugin = E2.plugins.three_meshline_material = function(core) {
	AbstractThreeMaterialPlugin.apply(this, arguments)

	this.desc = 'THREE.js MeshLine Material'

	this.input_slots = [
		{ name: 'color',		dt: core.datatypes.COLOR, def: new THREE.Color(0xffffff) },
		{ name: 'sizeAttenuation',	dt: core.datatypes.BOOL,  def: true },
		{ name: 'lineWidth',		dt: core.datatypes.FLOAT, def: 0.01 },
		{ name: 'near',			dt: core.datatypes.FLOAT, def: 0.01 },
		{ name: 'far',			dt: core.datatypes.FLOAT, def: 1000 },
		{ name: 'depthTest',		dt: core.datatypes.BOOL,  def: true },
		{ name: 'wireframe',		dt: core.datatypes.BOOL,  def: false }
	].concat(this.input_slots)

	this.output_slots = [{
		name: 'material',
		dt: core.datatypes.MATERIAL
	}]

	// Needed for the shader sizeAttenuation
	this.resolution = new THREE.Vector2(1, 1)

	// Get the parameters from the input_slots
	var params = {}
	this.input_slots.map(function(slot) {
		params[slot.name] = slot.def
	})
	params.resolution = this.resolution

	this.material = new THREE.MeshLineMaterial(params)
}

ThreeMeshLineMaterialPlugin.prototype = Object.create(AbstractThreeMaterialPlugin.prototype)

ThreeMeshLineMaterialPlugin.prototype.state_changed = function(ui) {
	// if ui is null, first time called, bind the resize
	if (!ui) {
		E2.core.on('resize', this.resize.bind(this))
	}
}

ThreeMeshLineMaterialPlugin.prototype.update_input = function(slot, data) {
	Plugin.prototype.update_input.apply(this, arguments)

	if ( slot.name === 'transparent'
	  || slot.name === 'wireframe'
	  || slot.name === 'side'
	  || slot.name === 'blending' ) {
		// Use the abstract material update method
		AbstractThreeMaterialPlugin.prototype.update_input.apply(this, arguments)
	} else {
		// Just update the shader directly
		this.material.uniforms[slot.name].value = data
	}
}

ThreeMeshLineMaterialPlugin.prototype.resize = function() {
	// Need to recalculate the resolution
	var canvasArea = E2.app.calculateCanvasArea()
	this.resolution.set(canvasArea.width, canvasArea.height)

	this.material.uniforms.resolution.value = this.resolution
}

})()

<commit_msg>Remove the unnecessary depthTest parameter<commit_after>(function() {
var ThreeMeshLineMaterialPlugin = E2.plugins.three_meshline_material = function(core) {
	AbstractThreeMaterialPlugin.apply(this, arguments)

	this.desc = 'THREE.js MeshLine Material'

	this.input_slots = [
		{ name: 'color',		dt: core.datatypes.COLOR, def: new THREE.Color(0xffffff) },
		{ name: 'sizeAttenuation',	dt: core.datatypes.BOOL,  def: true },
		{ name: 'lineWidth',		dt: core.datatypes.FLOAT, def: 0.01 },
		{ name: 'near',			dt: core.datatypes.FLOAT, def: 0.01 },
		{ name: 'far',			dt: core.datatypes.FLOAT, def: 1000 },
		{ name: 'wireframe',		dt: core.datatypes.BOOL,  def: false }
	].concat(this.input_slots)

	this.output_slots = [{
		name: 'material',
		dt: core.datatypes.MATERIAL
	}]

	// Needed for the shader sizeAttenuation
	this.resolution = new THREE.Vector2(1, 1)

	// Get the parameters from the input_slots
	var params = {}
	this.input_slots.map(function(slot) {
		params[slot.name] = slot.def
	})
	params.resolution = this.resolution

	this.material = new THREE.MeshLineMaterial(params)
}

ThreeMeshLineMaterialPlugin.prototype = Object.create(AbstractThreeMaterialPlugin.prototype)

ThreeMeshLineMaterialPlugin.prototype.state_changed = function(ui) {
	// if ui is null, first time called, bind the resize
	if (!ui) {
		E2.core.on('resize', this.resize.bind(this))
	}
}

ThreeMeshLineMaterialPlugin.prototype.update_input = function(slot, data) {
	Plugin.prototype.update_input.apply(this, arguments)

	if ( slot.name === 'transparent'
	  || slot.name === 'wireframe'
	  || slot.name === 'side'
	  || slot.name === 'blending' ) {
		// Use the abstract material update method
		AbstractThreeMaterialPlugin.prototype.update_input.apply(this, arguments)
	} else {
		// Just update the shader directly
		this.material.uniforms[slot.name].value = data
	}
}

ThreeMeshLineMaterialPlugin.prototype.resize = function() {
	// Need to recalculate the resolution
	var canvasArea = E2.app.calculateCanvasArea()
	this.resolution.set(canvasArea.width, canvasArea.height)

	this.material.uniforms.resolution.value = this.resolution
}

})()

<|endoftext|>