<commit_before>require('es6-shim');

var assert = require('assert');
var events = require('events');
var path = require('path');
var util = require('util');
var tlv = require('tlv');
var TLV = tlv.TLV;
var PluginLoader = require('plugin-loader').PluginLoader;
var DeviceInfo = require('./device_info').DeviceInfo;
var Datastore = require('nedb');

var DeviceType = {
  TEMPERATURE_SENSOR: 0x0001,
  MOISTURE_SENSOR: 0x0002,
  VELOCITY_SENSOR: 0x0003,
};

var CommandType = {
  GET_DEVICE_INFO: 0xA0,
  CONFIGURE: 0xA1,
  GET_DATA: 0xA2,
  PERFORM: 0xA3,
  SUBSCRIBE: 0xA4,
  UNSUBSCRIBE: 0xA5,
};

exports.Device = function(connector, protocolInfo, subdeviceID) {
  this.connector = connector;
  this.protocolInfo = protocolInfo;
  this.physicalID = this.connector + "|" + this.protocolInfo.id;
  this.subdeviceID = subdeviceID;

  this.id = this.subdeviceID ? (this.physicalID + "#" + this.subdeviceID) : this.physicalID;
  this.deviceInfo = null;
  this.paired = false;
}

exports.DeviceCommand = function(device, type, parameters, callback) {
  this.device = device;
  this.type = type;
  this.parameters = parameters;
  this.callback = callback;
}

exports.DeviceManager = function(config) {
  this._connectorLoader = new PluginLoader(config.connectorDirectories);
  this._config = config;
  this._pairedDevices = new Datastore({ filename: path.join(config.persistencyPath, 'devices.nedb') });
  this._unpairedDevices = [];
  this._subscriptions = new Map();
}

util.inherits(exports.DeviceManager, events.EventEmitter);

exports.DeviceType = DeviceType;
exports.CommandType = CommandType;
exports.DeviceManager.prototype.DeviceType = DeviceType;
exports.DeviceManager.prototype.CommandType = CommandType;

exports.DeviceManager.prototype.start = function(done) {
  this._pairedDevices.loadDatabase(function() {
    this._connectorLoader.on('pluginLoaded', initializeConnector.bind(this))
    this._connectorLoader.on('pluginUnloaded', destroyConnector.bind(this))
    this._connectorLoader.startMonitoring();

    this._pairedDevices.ensureIndex({ fieldName: 'id', unique: true });
    this._pairedDevices.ensureIndex({ fieldName: 'type' });

    done();
  }.bind(this));
}

exports.DeviceManager.prototype.pair = function(device, pairingData, cb) {
  this._connectorLoader.loadedModules[device.connector].pair(device, pairingData, handlePairedDevice.bind(this, cb));
}

exports.DeviceManager.prototype.unpair = function(device, cb) {
  this._connectorLoader.loadedModules[device.connector].unpair(device, handleUnpairedDevice.bind(this, cb));
}

exports.DeviceManager.prototype.discoverDevices = function() {
  this._unpairedDevices.length = 0;

  for (var connectorName in this._connectorLoader.loadedModules) {
    this._connectorLoader.loadedModules[connectorName].discoverDevices();
  }
}

exports.DeviceManager.prototype.getPairedDevices = function(cb) {
  this._pairedDevices.find({}, function (err, devices) {
    cb(devices);
  });
}

exports.DeviceManager.prototype.getUnpairedDevices = function(cb) {
  cb(this._unpairedDevices.filter(function(device) { return device.deviceInfo != null; }));
}

exports.DeviceManager.prototype.getDevicesByType = function(types, cb) {
  if (!types || types.length == 0) {
    this.getPairedDevices(cb);
    return;
  } else if (!util.isArray(types)) {
    types = [ types ];
  }

  this._pairedDevices.find({ 'deviceInfo.type': { $in: types }}, function (err, foundDevices) {
    cb(foundDevices);
  });
}

exports.DeviceManager.prototype.getDevicesByConnector = function(connector, cb) {
  this._pairedDevices.find({ connector: connector }, function (err, foundDevices) {
    cb(foundDevices);
  });
}

exports.DeviceManager.prototype.getDeviceByID = function(id, cb) {
  this._pairedDevices.findOne({ id: id }, function (err, device) {
    cb(device);
  });
}

exports.DeviceManager.prototype.getUnpairedDeviceByID = function(id, cb) {
  var device = this._unpairedDevices.find(function(device) {
    return device.id == id;
  });

  cb(device);
}

exports.DeviceManager.prototype.getDeviceInfo = function(device, cb) {
  this.sendCommands([ this.createGetDeviceInfoCommand(device, handleGetInfoResponse.bind(this)) ]);
}

exports.DeviceManager.prototype.createGetDeviceInfoCommand = function(device, cb) {
  return new exports.DeviceCommand(device, CommandType.GET_DEVICE_INFO, [], cb);
}

exports.DeviceManager.prototype.configureDevice = function(device, options, cb) {
  this.sendCommands([ this.createConfigureCommand(device, options, cb) ]);
}

exports.DeviceManager.prototype.createConfigureCommand = function(device, options, cb) {
  var params = [ new TLV(0xA0, options) ];

  return new exports.DeviceCommand(device, CommandType.CONFIGURE, params, cb);
}

exports.DeviceManager.prototype.getData = function(device, options, cb) {
  this.sendCommands([ this.createGetDataCommand(device, options, cb) ]);
}

exports.DeviceManager.prototype.createGetDataCommand = function(device, options, cb) {
  if (options instanceof Function) {
    cb = options;
    options = [];
  }

  var params = [];

  if (options.length > 0) {
    params.push(new TLV(0xA0, options));
  }

  return new exports.DeviceCommand(device, CommandType.GET_DATA, params, cb);
}

exports.DeviceManager.prototype.perform = function(device, action, options, cb) {
  this.sendCommands([ this.createPerformCommand(device, action, options, cb) ]);
}

exports.DeviceManager.prototype.createPerformCommand = function(device, action, options, cb) {
  if (options instanceof Function) {
    cb = options;
    options = [];
  }

  var params = [ new TLV(0x81, new Buffer([action])) ];

  if (options.length > 0) {
    params.push(new TLV(0xA0, options));
  }

  return new exports.DeviceCommand(device, CommandType.PERFORM, params, cb);
}

exports.DeviceManager.prototype.subscribe = function(device, notificationIDs, cb, errorCallback) {
  var subscribedNotificationsForDevice = this._subscriptions.get(device);
  var stillUnsubscribedNotifications = [];

  if (!subscribedNotificationsForDevice) {
    subscribedNotificationsForDevice = new Map();
    this._subscriptions.set(device, subscribedNotificationsForDevice);
  }

  for (var i = 0, len = notificationsIDs.length; i < len; i++) {
    var subscribers = subscribedNotificationsForDevice.get(notificationsIDs[i]);

    if (!subscribers) {
      subscribers = [];
      subscribedNotificationsForDevice.set(notificationsIDs[i], subscribers);
    }

    if (subscribers.length == 0) {
      stillUnsubscribedNotifications.push(notificationsIDs[i]);
    }

    if (subscribers.indexOf(cb) == -1) {
      subscribers.push(cb);
    }
  }

  if (stillUnsubscribedNotifications.length > 0) {
    this.sendCommands([ createSubscribeCommand(device, stillUnsubscribedNotifications, handleSubscribtionResponse.bind(this, errorCallback)) ])
  }
}

exports.DeviceManager.prototype.createSubscribeCommand = function(device, notificationIDs, cb) {
  var params = [ new TLV(0x81, tlv.encodeTags(notificationIDs)) ];
  return new exports.DeviceCommand(device, CommandType.SUBSCRIBE, params, cb);
}

exports.DeviceManager.prototype.unsubscribe = function(device, notificationIDs, cb, errorCallback) {
  var subscribedNotificationsForDevice = this._subscriptions.get(device);
  var notificationsToUnsubscribe = [];

  if (!subscribedNotificationsForDevice) {
    return;
  }

  for (var i = 0, len = notificationsIDs.length; i < len; i++) {
    var subscribers = subscribedNotificationsForDevice.get(notificationsIDs[i]);

    if (subscribers) {
      var idx = subscribers.indexOf(cb);

      if (idx != -1) {
        subscribers.splice(idx, 1);
      }

      if (subscribers.length == 0) {
        notificationsToUnsubscribe.push(notificationsIDs[i]);
      }
    }
  }

  if (notificationsToUnsubscribe.length > 0) {
    this.sendCommands([ createUnsubscribeCommand(device, notificationsToUnsubscribe, handleSubscribtionResponse.bind(this, errorCallback)) ])
  }
}

exports.DeviceManager.prototype.createUnsubscribeCommand = function(device, notificationIDs, cb) {
  var params = [ new TLV(0x81, tlv.encodeTags(notificationIDs)) ];
  return new exports.DeviceCommand(device, CommandType.UNSUBSCRIBE, params, cb);
}

exports.DeviceManager.prototype.sendCommands = function(commands) {
  var groupedCommands = groupCommandsByPhysicalDevice(commands);

  for (var physicalDeviceID in groupedCommands) {
    var commands = groupedCommands[physicalDeviceID];
    this.sendPacket(commands[0].device, commands);
  }
}

exports.DeviceManager.prototype.sendPacket = function(physicalDevice, commands) {
  this._connectorLoader.loadedModules[physicalDevice.connector].send(physicalDevice, this.createPacket(commands).encode(), processResponse.bind(this, commands));
}

exports.DeviceManager.prototype.createPacket = function(commands) {
  var packet = new TLV(0xE0, []);

  for (var i = 0, len = commands.length; i < len; i++) {
    var command = commands[i];
    var commandTLV = new TLV(command.type, []);

    if (command.device.subdeviceID !== undefined) {
      commandTLV.value.push(new TLV(0x80, new Buffer([command.device.subdeviceID])));
    }

    Array.prototype.push.apply(commandTLV.value, command.parameters);
    packet.value.push(commandTLV);
  };

  return packet;
}

function processResponse(commands, response) {
  var responseTLV = tlv.parse(response);

  assert.equal(commands.length, responseTLV.value.length);

  for(var i = 0, len = commands.length; i < len; i++) {
    commands[i].callback(commands[i], responseTLV.value[i]);
  }
}

function handleGetInfoResponse(command, responseTLV) {
  var error = responseTLV.getFirstChild(0x9E);

  if (error) {
    //do something, maybe repeat command?
  }

  var device = command.device;
  device.deviceInfo = new DeviceInfo(responseTLV);

  this.emit('deviceDiscovered', command.device);

  if (device.deviceInfo.subdeviceCount) {
    var getInfoCommands = [];

    for(var i = 0; i < device.deviceInfo.subdeviceCount; i++) {
      var child = new exports.Device(device.connector, device.protocolInfo, i);
      this._unpairedDevices.push(child);

      getInfoCommands.push(this.createGetDeviceInfoCommand(child, handleGetInfoResponse.bind(this, cb)));
    }

    this.sendCommands(getInfoCommands);
  }
}

function handleSubscribtionResponse(errorCallback, command, responseTLV) {
  var error = responseTLV.getFirstChild(0x9E);

  if (error && errorCallback) {
    errorCallback(error.getIntValue());
  }
}

function handlePairedDevice(cb, device, err) {
  if (err) {
    cb(device, err);
    return;
  }

  device.paired = true;

  var idx = this._unpairedDevices.indexOf(device);

  if (idx != -1) {
    this._unpairedDevices.splice(idx);
  }

  this._pairedDevices.insert(device, function(insertErr, insertedDevice) {
    if (cb) {
      cb(insertedDevice);
    }

    this.emit('devicePaired', insertedDevice);
  }.bind(this));
}

function handleUnpairedDevice(cb, device, err) {
  if (err) {
    cb(device, err);
    return;
  }

  device.paired = false;

  db.remove({ id: device.id }, {}, function (err, numRemoved) {
    this._unpairedDevices.push(device);

    if (cb) {
      cb(device);
    }

    this.emit('deviceUnpaired', device);
  }.bind(this));
}

function handleDiscoveredDevice(connectorName, deviceProtocolInfo) {
  var newDevice = new exports.Device(connectorName, deviceProtocolInfo);
  var newID = newDevice.id;

  this.getUnpairedDeviceByID(newID, function(device) {
    if(!device) {
      this._unpairedDevices.push(newDevice);
      this.getDeviceInfo(newDevice);
    }
  }.bind(this));
}

function groupCommandsByPhysicalDevice(commands) {
  var groupedCommands = {};
  for (var i = 0, len = commands.length; i < len; i++) {
    var command = commands[i];
    var mapEntry = groupedCommands[command.device.physicalID];

    if (!mapEntry) {
      mapEntry = [];
      groupedCommands[command.device.physicalID] = mapEntry;
    }

    mapEntry.push(command);
  };

  return groupedCommands;
}

function initializeConnector(connectorName, connector) {
  console.log('Loading connector: ' + connectorName);
  connector.on('deviceDiscovered', handleDiscoveredDevice.bind(this, connectorName));

  this.getDevicesByConnector(connectorName, function(pairedDevices) {
    connector.start(pairedDevices);
  }.bind(this));
}

function destroyConnector(connectorName, connector) {
  connector.removeAllListeners();
  console.log('Unloading connector: ' + connectorName);
}

exports.groupCommandsByPhysicalDevice = groupCommandsByPhysicalDevice;<commit_msg>corrected copy&paste error<commit_after>require('es6-shim');

var assert = require('assert');
var events = require('events');
var path = require('path');
var util = require('util');
var tlv = require('tlv');
var TLV = tlv.TLV;
var PluginLoader = require('plugin-loader').PluginLoader;
var DeviceInfo = require('./device_info').DeviceInfo;
var Datastore = require('nedb');

var DeviceType = {
  TEMPERATURE_SENSOR: 0x0001,
  MOISTURE_SENSOR: 0x0002,
  VELOCITY_SENSOR: 0x0003,
};

var CommandType = {
  GET_DEVICE_INFO: 0xA0,
  CONFIGURE: 0xA1,
  GET_DATA: 0xA2,
  PERFORM: 0xA3,
  SUBSCRIBE: 0xA4,
  UNSUBSCRIBE: 0xA5,
};

exports.Device = function(connector, protocolInfo, subdeviceID) {
  this.connector = connector;
  this.protocolInfo = protocolInfo;
  this.physicalID = this.connector + "|" + this.protocolInfo.id;
  this.subdeviceID = subdeviceID;

  this.id = this.subdeviceID ? (this.physicalID + "#" + this.subdeviceID) : this.physicalID;
  this.deviceInfo = null;
  this.paired = false;
}

exports.DeviceCommand = function(device, type, parameters, callback) {
  this.device = device;
  this.type = type;
  this.parameters = parameters;
  this.callback = callback;
}

exports.DeviceManager = function(config) {
  this._connectorLoader = new PluginLoader(config.connectorDirectories);
  this._config = config;
  this._pairedDevices = new Datastore({ filename: path.join(config.persistencyPath, 'devices.nedb') });
  this._unpairedDevices = [];
  this._subscriptions = new Map();
}

util.inherits(exports.DeviceManager, events.EventEmitter);

exports.DeviceType = DeviceType;
exports.CommandType = CommandType;
exports.DeviceManager.prototype.DeviceType = DeviceType;
exports.DeviceManager.prototype.CommandType = CommandType;

exports.DeviceManager.prototype.start = function(done) {
  this._pairedDevices.loadDatabase(function() {
    this._connectorLoader.on('pluginLoaded', initializeConnector.bind(this))
    this._connectorLoader.on('pluginUnloaded', destroyConnector.bind(this))
    this._connectorLoader.startMonitoring();

    this._pairedDevices.ensureIndex({ fieldName: 'id', unique: true });
    this._pairedDevices.ensureIndex({ fieldName: 'type' });

    done();
  }.bind(this));
}

exports.DeviceManager.prototype.pair = function(device, pairingData, cb) {
  this._connectorLoader.loadedModules[device.connector].pair(device, pairingData, handlePairedDevice.bind(this, cb));
}

exports.DeviceManager.prototype.unpair = function(device, cb) {
  this._connectorLoader.loadedModules[device.connector].unpair(device, handleUnpairedDevice.bind(this, cb));
}

exports.DeviceManager.prototype.discoverDevices = function() {
  this._unpairedDevices.length = 0;

  for (var connectorName in this._connectorLoader.loadedModules) {
    this._connectorLoader.loadedModules[connectorName].discoverDevices();
  }
}

exports.DeviceManager.prototype.getPairedDevices = function(cb) {
  this._pairedDevices.find({}, function (err, devices) {
    cb(devices);
  });
}

exports.DeviceManager.prototype.getUnpairedDevices = function(cb) {
  cb(this._unpairedDevices.filter(function(device) { return device.deviceInfo != null; }));
}

exports.DeviceManager.prototype.getDevicesByType = function(types, cb) {
  if (!types || types.length == 0) {
    this.getPairedDevices(cb);
    return;
  } else if (!util.isArray(types)) {
    types = [ types ];
  }

  this._pairedDevices.find({ 'deviceInfo.type': { $in: types }}, function (err, foundDevices) {
    cb(foundDevices);
  });
}

exports.DeviceManager.prototype.getDevicesByConnector = function(connector, cb) {
  this._pairedDevices.find({ connector: connector }, function (err, foundDevices) {
    cb(foundDevices);
  });
}

exports.DeviceManager.prototype.getDeviceByID = function(id, cb) {
  this._pairedDevices.findOne({ id: id }, function (err, device) {
    cb(device);
  });
}

exports.DeviceManager.prototype.getUnpairedDeviceByID = function(id, cb) {
  var device = this._unpairedDevices.find(function(device) {
    return device.id == id;
  });

  cb(device);
}

exports.DeviceManager.prototype.getDeviceInfo = function(device, cb) {
  this.sendCommands([ this.createGetDeviceInfoCommand(device, handleGetInfoResponse.bind(this)) ]);
}

exports.DeviceManager.prototype.createGetDeviceInfoCommand = function(device, cb) {
  return new exports.DeviceCommand(device, CommandType.GET_DEVICE_INFO, [], cb);
}

exports.DeviceManager.prototype.configureDevice = function(device, options, cb) {
  this.sendCommands([ this.createConfigureCommand(device, options, cb) ]);
}

exports.DeviceManager.prototype.createConfigureCommand = function(device, options, cb) {
  var params = [ new TLV(0xA0, options) ];

  return new exports.DeviceCommand(device, CommandType.CONFIGURE, params, cb);
}

exports.DeviceManager.prototype.getData = function(device, options, cb) {
  this.sendCommands([ this.createGetDataCommand(device, options, cb) ]);
}

exports.DeviceManager.prototype.createGetDataCommand = function(device, options, cb) {
  if (options instanceof Function) {
    cb = options;
    options = [];
  }

  var params = [];

  if (options.length > 0) {
    params.push(new TLV(0xA0, options));
  }

  return new exports.DeviceCommand(device, CommandType.GET_DATA, params, cb);
}

exports.DeviceManager.prototype.perform = function(device, action, options, cb) {
  this.sendCommands([ this.createPerformCommand(device, action, options, cb) ]);
}

exports.DeviceManager.prototype.createPerformCommand = function(device, action, options, cb) {
  if (options instanceof Function) {
    cb = options;
    options = [];
  }

  var params = [ new TLV(0x81, new Buffer([action])) ];

  if (options.length > 0) {
    params.push(new TLV(0xA0, options));
  }

  return new exports.DeviceCommand(device, CommandType.PERFORM, params, cb);
}

exports.DeviceManager.prototype.subscribe = function(device, notificationIDs, cb, errorCallback) {
  var subscribedNotificationsForDevice = this._subscriptions.get(device);
  var stillUnsubscribedNotifications = [];

  if (!subscribedNotificationsForDevice) {
    subscribedNotificationsForDevice = new Map();
    this._subscriptions.set(device, subscribedNotificationsForDevice);
  }

  for (var i = 0, len = notificationsIDs.length; i < len; i++) {
    var subscribers = subscribedNotificationsForDevice.get(notificationsIDs[i]);

    if (!subscribers) {
      subscribers = [];
      subscribedNotificationsForDevice.set(notificationsIDs[i], subscribers);
    }

    if (subscribers.length == 0) {
      stillUnsubscribedNotifications.push(notificationsIDs[i]);
    }

    if (subscribers.indexOf(cb) == -1) {
      subscribers.push(cb);
    }
  }

  if (stillUnsubscribedNotifications.length > 0) {
    this.sendCommands([ createSubscribeCommand(device, stillUnsubscribedNotifications, handleSubscribtionResponse.bind(this, errorCallback)) ])
  }
}

exports.DeviceManager.prototype.createSubscribeCommand = function(device, notificationIDs, cb) {
  var params = [ new TLV(0x81, tlv.encodeTags(notificationIDs)) ];
  return new exports.DeviceCommand(device, CommandType.SUBSCRIBE, params, cb);
}

exports.DeviceManager.prototype.unsubscribe = function(device, notificationIDs, cb, errorCallback) {
  var subscribedNotificationsForDevice = this._subscriptions.get(device);
  var notificationsToUnsubscribe = [];

  if (!subscribedNotificationsForDevice) {
    return;
  }

  for (var i = 0, len = notificationsIDs.length; i < len; i++) {
    var subscribers = subscribedNotificationsForDevice.get(notificationsIDs[i]);

    if (subscribers) {
      var idx = subscribers.indexOf(cb);

      if (idx != -1) {
        subscribers.splice(idx, 1);
      }

      if (subscribers.length == 0) {
        notificationsToUnsubscribe.push(notificationsIDs[i]);
      }
    }
  }

  if (notificationsToUnsubscribe.length > 0) {
    this.sendCommands([ createUnsubscribeCommand(device, notificationsToUnsubscribe, handleSubscribtionResponse.bind(this, errorCallback)) ])
  }
}

exports.DeviceManager.prototype.createUnsubscribeCommand = function(device, notificationIDs, cb) {
  var params = [ new TLV(0x81, tlv.encodeTags(notificationIDs)) ];
  return new exports.DeviceCommand(device, CommandType.UNSUBSCRIBE, params, cb);
}

exports.DeviceManager.prototype.sendCommands = function(commands) {
  var groupedCommands = groupCommandsByPhysicalDevice(commands);

  for (var physicalDeviceID in groupedCommands) {
    var commands = groupedCommands[physicalDeviceID];
    this.sendPacket(commands[0].device, commands);
  }
}

exports.DeviceManager.prototype.sendPacket = function(physicalDevice, commands) {
  this._connectorLoader.loadedModules[physicalDevice.connector].send(physicalDevice, this.createPacket(commands).encode(), processResponse.bind(this, commands));
}

exports.DeviceManager.prototype.createPacket = function(commands) {
  var packet = new TLV(0xE0, []);

  for (var i = 0, len = commands.length; i < len; i++) {
    var command = commands[i];
    var commandTLV = new TLV(command.type, []);

    if (command.device.subdeviceID !== undefined) {
      commandTLV.value.push(new TLV(0x80, new Buffer([command.device.subdeviceID])));
    }

    Array.prototype.push.apply(commandTLV.value, command.parameters);
    packet.value.push(commandTLV);
  };

  return packet;
}

function processResponse(commands, response) {
  var responseTLV = tlv.parse(response);

  assert.equal(commands.length, responseTLV.value.length);

  for(var i = 0, len = commands.length; i < len; i++) {
    commands[i].callback(commands[i], responseTLV.value[i]);
  }
}

function handleGetInfoResponse(command, responseTLV) {
  var error = responseTLV.getFirstChild(0x9E);

  if (error) {
    //do something, maybe repeat command?
  }

  var device = command.device;
  device.deviceInfo = new DeviceInfo(responseTLV);

  this.emit('deviceDiscovered', command.device);

  if (device.deviceInfo.subdeviceCount) {
    var getInfoCommands = [];

    for(var i = 0; i < device.deviceInfo.subdeviceCount; i++) {
      var child = new exports.Device(device.connector, device.protocolInfo, i);
      this._unpairedDevices.push(child);

      getInfoCommands.push(this.createGetDeviceInfoCommand(child, handleGetInfoResponse.bind(this, cb)));
    }

    this.sendCommands(getInfoCommands);
  }
}

function handleSubscribtionResponse(errorCallback, command, responseTLV) {
  var error = responseTLV.getFirstChild(0x9E);

  if (error && errorCallback) {
    errorCallback(error.getIntValue());
  }
}

function handlePairedDevice(cb, device, err) {
  if (err) {
    cb(device, err);
    return;
  }

  device.paired = true;

  var idx = this._unpairedDevices.indexOf(device);

  if (idx != -1) {
    this._unpairedDevices.splice(idx);
  }

  this._pairedDevices.insert(device, function(insertErr, insertedDevice) {
    if (cb) {
      cb(insertedDevice);
    }

    this.emit('devicePaired', insertedDevice);
  }.bind(this));
}

function handleUnpairedDevice(cb, device, err) {
  if (err) {
    cb(device, err);
    return;
  }

  device.paired = false;

  this._pairedDevices.remove({ id: device.id }, {}, function (err, numRemoved) {
    this._unpairedDevices.push(device);

    if (cb) {
      cb(device);
    }

    this.emit('deviceUnpaired', device);
  }.bind(this));
}

function handleDiscoveredDevice(connectorName, deviceProtocolInfo) {
  var newDevice = new exports.Device(connectorName, deviceProtocolInfo);
  var newID = newDevice.id;

  this.getUnpairedDeviceByID(newID, function(device) {
    if(!device) {
      this._unpairedDevices.push(newDevice);
      this.getDeviceInfo(newDevice);
    }
  }.bind(this));
}

function groupCommandsByPhysicalDevice(commands) {
  var groupedCommands = {};
  for (var i = 0, len = commands.length; i < len; i++) {
    var command = commands[i];
    var mapEntry = groupedCommands[command.device.physicalID];

    if (!mapEntry) {
      mapEntry = [];
      groupedCommands[command.device.physicalID] = mapEntry;
    }

    mapEntry.push(command);
  };

  return groupedCommands;
}

function initializeConnector(connectorName, connector) {
  console.log('Loading connector: ' + connectorName);
  connector.on('deviceDiscovered', handleDiscoveredDevice.bind(this, connectorName));

  this.getDevicesByConnector(connectorName, function(pairedDevices) {
    connector.start(pairedDevices);
  }.bind(this));
}

function destroyConnector(connectorName, connector) {
  connector.removeAllListeners();
  console.log('Unloading connector: ' + connectorName);
}

exports.groupCommandsByPhysicalDevice = groupCommandsByPhysicalDevice;<|endoftext|>


<commit_before>//TODO:
//      Make sure it works for any number of bands >= 1.
//      - currently things get overlappy and weird when the bandSize is changed to very different values and then updated.
//      Test with only negative values; only positive values.
//      Test with different zero levels of all possibilities.
//      strange things happen for various bandSize's and negative and positive values.
//      zoom buttons do not work together with the zoom function (likely due to the centering issue).
//      fix centering issue with zooming into the plot.
//      get zooming and especially panning to work on the iPad.
//      use boxes (or some way so that we can treat the parts individually)
//      put multiple plots of real data beside eachother
//      synchronize the zooming of two plots
//      plot peaks vs. lows for large regions (may have to abandon horizon for this to look good).
//      make nice bandSize transitions.
//      fix the difference in zoomings between the axis and the plot.
//      BIG ITEM:
//      - Convert everything to use HTML5 canvas instead. This renders an image, which means we lose things like hover events, but real-time manipulation should be much quicker.
//        - resizing might be weird and difficult again...
//        - can perhaps use BOTH svg and canvas rendering like this page does: http://www.jasondavies.com/tree-of-life/
//      make margins dynamic, or at least based on whether or not there are specific axes.
//      to change the number of bands with an animation, we need to store a d0 as well as a d1; a current state and a future state, so that the new bands can start off like the others are, then transition to the new state.

var horizonChart = function () {
  var bandSize = 3.5; // maybe have this constant band size instead of setting the number of bands.
  var outlinesOrNot = true;

  var margins = {top: 0, left: 25, bottom: 25, right: 25};

  var height = 50;
  var width = d3.max([window.innerWidth, screen.width]);
  var zeroPoint = 0; //TODO: use scales instead? Might make things WAY simpler if we scale the data

  var numOfPositiveBands;
  var numOfNegativeBands;
  var numOfMostBands;

  var bkgrect;
  var frgrect;
  var defclip;
  var xAxisContainer;
  var xAxis;
  var yAxisContainer;
  var yAxis;
  var xScale;
  var yScale;
  var xAxisScale;

  var chart;
  var paths;
  var d3area1;
  var d0;

  var slctn; // Save the selection so that my.update() works.

  var my = function (selection) {
    //console.log(document.getElementById("charts").offsetWidth + " : " + window.innerWidth);
    slctn = selection; // Save the selection so that my.update() works.

    realWidth = width - margins.right - margins.left;
    //width = width - margins.right - margins.left;

    selection.each(function (data) {

      numOfPositiveBands = (d3.max(data) > zeroPoint) ? Math.ceil(Math.abs(d3.max(data) - zeroPoint) / bandSize) : 0; // the closest to mod bandSize, rounded up.
      numOfNegativeBands = (d3.min(data) < zeroPoint) ? Math.ceil(Math.abs(zeroPoint - d3.min(data)) / bandSize) : 0;
      numOfMostBands = d3.max([numOfPositiveBands, numOfNegativeBands]);

      if (!xScale) { xScale = d3.scale.linear().domain([0, data.length]); }
      xScale
        .range([0, realWidth + (realWidth / (data.length - 1))]); // So that the furthest-right point is at the right edge of the plot

      if (!xAxisScale) { xAxisScale = d3.scale.linear().domain([0, data.length - 1]); } //different than xScale because we want the right-most point to be at the right edge of the chart
      xAxisScale
        .range([0, realWidth]);

      if (!yScale){ yScale = d3.scale.linear(); }
      yScale
        .domain([zeroPoint, d3.max([zeroPoint, numOfMostBands * bandSize])])
        .range([height * numOfPositiveBands, 0]);

      var fillScale = d3.scale.linear()
        .domain([0, numOfMostBands])
        .rangeRound([255, 0]);

      if (!d3area1){ d3area1 = d3.svg.area(); }
      var d0 = d3area1
        .x(function (d, i) { return xScale(i); })
        .y1(function (d, i) { return yScale(d); })
        .y0(height * numOfPositiveBands) //TODO: change this to both Pos and Neg or something ??? Probably perfect how it is.
        //              .interpolate("cardinal");
        .interpolate("linear")(data);

      chart = d3.select(this); //TODO: Since we're using a .call(), "this" is the svg element.

      //Set it's container's dimensions
      selection
        .attr("height", height + margins.bottom)
        .attr("width", width);

      //Set the chart's dimensions
      chart
        .attr("width", width - 10) //TODO: magic numbers to get rid of scroll bars
        .attr("height", height + margins.bottom);

      //Allow dragging and zooming.
      //console.log("before: " + xScale.domain());
      chart.call(d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([0.125, 8]).on("zoom", my.zoom));
      //selection.call(d3.behavior.zoom().x(xAxisScale));
      //console.log("after: " + xScale.domain());

      //Draw the background for the chart
      if (!bkgrect) { bkgrect = chart.insert("svg:rect"); }
      bkgrect
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("height", height)
        .attr("class", "bkgrect")
        .attr("transform", "translate(" + margins.left + ", 0)")
        .style("fill", "#FFF");

      //Make the clipPath (for cropping the paths)
      if (!defclip) { defclip = chart.insert("defs").append("clipPath").attr("id", "clip").append("rect"); }
      defclip
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("transform", "translate(" + margins.left + ", 0)")
        .attr("height", height);

      //Apply the clipPath
      paths = !paths ? chart.append("g") : paths;
      paths
        .attr("clip-path", "url(#clip)")
        .attr("class", "paths")
        .attr("height", height);

      var currentSelection;

      //Make and render the Positive curves.
      currentSelection = paths.selectAll(".posPath")
        .data(d3.range(numOfMostBands));

      //update
      currentSelection
        .attr("fill", function (d, i) { return "rgba(255, " + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        //.style("cursor", "help")
        .style("stroke", "#000")
        //.transition().duration(1000)
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (i - numOfMostBands + 1) * height + ")"; });

      //enter
      currentSelection.enter().append("path")
        .attr("class", "posPath")
        .attr("fill", function (d, i) { return "rgba(255, " + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (i - numOfMostBands + 1) * height + ")"; });

      //Make and render the Negative curves.
      currentSelection = paths.selectAll(".negPath")
        .data(d3.range(numOfMostBands, 0, -1));

      //update
      currentSelection
        .attr("class", "negPath")
        .attr("fill", function (d, i) { return "rgba(" + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 255, 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        //.transition().duration(1000)
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (d - (numOfMostBands * 2)) * height + ")"; });

      //enter
      currentSelection.enter().append("path")
        .attr("class", "negPath")
        .attr("fill", function (d, i) { return "rgba(" + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 255, 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (d - (numOfMostBands * 2)) * height + ")"; });

      // Draw Axes
      xAxis = d3.svg.axis()
        .scale(xAxisScale).orient("bottom");
      yAxis = d3.svg.axis().scale(yScale).orient("bottom");

      if (!xAxisContainer) { xAxisContainer = chart.append("svg:g"); }
      xAxisContainer.attr("class", "x axis")
        .attr("transform", "translate(" + margins.left + "," + height + ")");
      xAxisContainer.transition().duration(1000).call(xAxis);

      //Draw the outline for the chart
      if (!frgrect) { frgrect = chart.append("svg:rect"); }
      frgrect
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("height", height)
        .attr("class", "frgrect")
        .style("fill", "rgba(0,0,0,0)")
        .style("stroke-width", 3)
        .attr("transform", "translate(" + margins.left + ", 0)")
        .style("stroke", "#000");

    });
  }

  // == Getters and Setters ==

  my.width = function (value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  }

  my.height = function (value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  }

  my.bandSize = function (value) {
    if (!arguments.length) return bandSize;
    bandSize = value;
    return my;
  }

  my.outlinesOrNot = function (value) {
    if (!arguments.length) return outlinesOrNot;
    outlinesOrNot = value;
    return my;
  }

  my.zoomout = function () {
    xScale.domain([0, xScale.domain()[1] * 2]); // TODO: modify a constant instead? That way we can re-do each domain each time without worrying or hacking around.
    xAxisScale.domain([0, xAxisScale.domain()[1] * 2]);
    return my;
  }

  my.zoomin = function () {
    xScale.domain([0, xScale.domain()[1] / 2]);
    xAxisScale.domain([0, xAxisScale.domain()[1] / 2]);
    return my;
  }

  my.zoom = function () {
    xAxisScale.domain(xScale.domain());
    xAxisContainer.call(xAxis);
    //yAxisContainer.call(yAxis);
    my.update();
  }

  my.update = function () {
    my(slctn);
  }

  return my;
}

//greatest value of screen.width and window.innerWidth ???
<commit_msg>Fixed horizion issue with incorrect scale scaling alignment.<commit_after>//TODO:
//      Make sure it works for any number of bands >= 1.
//      - currently things get overlappy and weird when the bandSize is changed to very different values and then updated.
//      Test with only negative values; only positive values.
//      Test with different zero levels of all possibilities.
//      strange things happen for various bandSize's and negative and positive values.
//      zoom buttons do not work together with the zoom function (likely due to the centering issue).
//      fix centering issue with zooming into the plot.
//      get zooming and especially panning to work on the iPad.
//      use boxes (or some way so that we can treat the parts individually)
//      put multiple plots of real data beside eachother
//      synchronize the zooming of two plots
//      plot peaks vs. lows for large regions (may have to abandon horizon for this to look good).
//      make nice bandSize transitions.
//      fix the difference in zoomings between the axis and the plot.
//      BIG ITEM:
//      - Convert everything to use HTML5 canvas instead. This renders an image, which means we lose things like hover events, but real-time manipulation should be much quicker.
//        - resizing might be weird and difficult again...
//        - can perhaps use BOTH svg and canvas rendering like this page does: http://www.jasondavies.com/tree-of-life/
//      make margins dynamic, or at least based on whether or not there are specific axes.
//      to change the number of bands with an animation, we need to store a d0 as well as a d1; a current state and a future state, so that the new bands can start off like the others are, then transition to the new state.

var horizonChart = function () {
  var bandSize = 3.5; // maybe have this constant band size instead of setting the number of bands.
  var outlinesOrNot = true;

  var margins = {top: 0, left: 25, bottom: 25, right: 25};

  var height = 50;
  var width = d3.max([window.innerWidth, screen.width]);
  var zeroPoint = 0; //TODO: use scales instead? Might make things WAY simpler if we scale the data

  var numOfPositiveBands;
  var numOfNegativeBands;
  var numOfMostBands;

  var bkgrect;
  var frgrect;
  var defclip;
  var xAxisContainer;
  var xAxis;
  var yAxisContainer;
  var yAxis;
  var xScale;
  var yScale;
  var xAxisScale;

  var chart;
  var paths;
  var d3area1;
  var d0;

  var slctn; // Save the selection so that my.update() works.

  var my = function (selection) {
    //console.log(document.getElementById("charts").offsetWidth + " : " + window.innerWidth);
    slctn = selection; // Save the selection so that my.update() works.

    realWidth = width - margins.right - margins.left;
    //width = width - margins.right - margins.left;

    selection.each(function (data) {

      numOfPositiveBands = (d3.max(data) > zeroPoint) ? Math.ceil(Math.abs(d3.max(data) - zeroPoint) / bandSize) : 0; // the closest to mod bandSize, rounded up.
      numOfNegativeBands = (d3.min(data) < zeroPoint) ? Math.ceil(Math.abs(zeroPoint - d3.min(data)) / bandSize) : 0;
      numOfMostBands = d3.max([numOfPositiveBands, numOfNegativeBands]);

      if (!xScale) { xScale = d3.scale.linear().domain([0, data.length - 1]); }
      xScale
        .range([0, realWidth]); // So that the furthest-right point is at the right edge of the plot

      if (!xAxisScale) { xAxisScale = d3.scale.linear().domain([0, data.length - 1]); } //different than xScale because we want the right-most point to be at the right edge of the chart
      xAxisScale
        .range([0, realWidth]);

      if (!yScale){ yScale = d3.scale.linear(); }
      yScale
        .domain([zeroPoint, d3.max([zeroPoint, numOfMostBands * bandSize])])
        .range([height * numOfPositiveBands, 0]);

      var fillScale = d3.scale.linear()
        .domain([0, numOfMostBands])
        .rangeRound([255, 0]);

      if (!d3area1){ d3area1 = d3.svg.area(); }
      var d0 = d3area1
        .x(function (d, i) { return xScale(i); })
        .y1(function (d, i) { return yScale(d); })
        .y0(height * numOfPositiveBands) //TODO: change this to both Pos and Neg or something ??? Probably perfect how it is.
        //              .interpolate("cardinal");
        .interpolate("linear")(data);

      chart = d3.select(this); //TODO: Since we're using a .call(), "this" is the svg element.

      //Set it's container's dimensions
      selection
        .attr("height", height + margins.bottom)
        .attr("width", width);

      //Set the chart's dimensions
      chart
        .attr("width", width - 10) //TODO: magic numbers to get rid of scroll bars
        .attr("height", height + margins.bottom);

      //Allow dragging and zooming.
      //console.log("before: " + xScale.domain());
      chart.call(d3.behavior.zoom().x(xScale).y(yScale).scaleExtent([0.125, 8]).on("zoom", my.zoom));
      //selection.call(d3.behavior.zoom().x(xAxisScale));
      //console.log("after: " + xScale.domain());

      //Draw the background for the chart
      if (!bkgrect) { bkgrect = chart.insert("svg:rect"); }
      bkgrect
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("height", height)
        .attr("class", "bkgrect")
        .attr("transform", "translate(" + margins.left + ", 0)")
        .style("fill", "#FFF");

      //Make the clipPath (for cropping the paths)
      if (!defclip) { defclip = chart.insert("defs").append("clipPath").attr("id", "clip").append("rect"); }
      defclip
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("transform", "translate(" + margins.left + ", 0)")
        .attr("height", height);

      //Apply the clipPath
      paths = !paths ? chart.append("g") : paths;
      paths
        .attr("clip-path", "url(#clip)")
        .attr("class", "paths")
        .attr("height", height);

      var currentSelection;

      //Make and render the Positive curves.
      currentSelection = paths.selectAll(".posPath")
        .data(d3.range(numOfMostBands));

      //update
      currentSelection
        .attr("fill", function (d, i) { return "rgba(255, " + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        //.style("cursor", "help")
        .style("stroke", "#000")
        //.transition().duration(1000)
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (i - numOfMostBands + 1) * height + ")"; });

      //enter
      currentSelection.enter().append("path")
        .attr("class", "posPath")
        .attr("fill", function (d, i) { return "rgba(255, " + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (i - numOfMostBands + 1) * height + ")"; });

      //Make and render the Negative curves.
      currentSelection = paths.selectAll(".negPath")
        .data(d3.range(numOfMostBands, 0, -1));

      //update
      currentSelection
        .attr("class", "negPath")
        .attr("fill", function (d, i) { return "rgba(" + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 255, 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        //.transition().duration(1000)
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (d - (numOfMostBands * 2)) * height + ")"; });

      //enter
      currentSelection.enter().append("path")
        .attr("class", "negPath")
        .attr("fill", function (d, i) { return "rgba(" + fillScale(i + 1) + ", " + fillScale(i + 1) + ", 255, 1)"; })
        .style("stroke-width", function () { return outlinesOrNot ? 1 : 0; })
        .style("stroke", "#000")
        .attr("d", d0)
        .attr("transform", function (d, i) {return "translate(" + margins.left + ", " + (d - (numOfMostBands * 2)) * height + ")"; });

      // Draw Axes
      xAxis = d3.svg.axis()
        .scale(xAxisScale).orient("bottom");
      yAxis = d3.svg.axis().scale(yScale).orient("bottom");

      if (!xAxisContainer) { xAxisContainer = chart.append("svg:g"); }
      xAxisContainer.attr("class", "x axis")
        .attr("transform", "translate(" + margins.left + "," + height + ")");
      xAxisContainer.transition().duration(1000).call(xAxis);

      //Draw the outline for the chart
      if (!frgrect) { frgrect = chart.append("svg:rect"); }
      frgrect
        //.transition().duration(1000)
        .attr("width", realWidth)
        .attr("height", height)
        .attr("class", "frgrect")
        .style("fill", "rgba(0,0,0,0)")
        .style("stroke-width", 3)
        .attr("transform", "translate(" + margins.left + ", 0)")
        .style("stroke", "#000");

    });
  }

  // == Getters and Setters ==

  my.width = function (value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  }

  my.height = function (value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  }

  my.bandSize = function (value) {
    if (!arguments.length) return bandSize;
    bandSize = value;
    return my;
  }

  my.outlinesOrNot = function (value) {
    if (!arguments.length) return outlinesOrNot;
    outlinesOrNot = value;
    return my;
  }

  my.zoomout = function () {
    xScale.domain([0, xScale.domain()[1] * 2]); // TODO: modify a constant instead? That way we can re-do each domain each time without worrying or hacking around.
    xAxisScale.domain([0, xAxisScale.domain()[1] * 2]);
    return my;
  }

  my.zoomin = function () {
    xScale.domain([0, xScale.domain()[1] / 2]);
    xAxisScale.domain([0, xAxisScale.domain()[1] / 2]);
    return my;
  }

  my.zoom = function () {
    xAxisScale.domain(xScale.domain());
    xAxisContainer.call(xAxis);
    //yAxisContainer.call(yAxis);
    my.update();
  }

  my.update = function () {
    my(slctn);
  }

  return my;
}

//greatest value of screen.width and window.innerWidth ???
<|endoftext|>


<commit_before>(function() {
	var scriptDep = document.createElement("script");
	scriptDep.src = "file:///Users/franklyn/code/black list for Grooveshark/blacklist-api.js";
	document.body.appendChild(scriptDep);
})();<commit_msg>Internal file for testing bookmark<commit_after><|endoftext|>


<commit_before>// VERSION 1.8.9

// This looks for file defaults.js in resource folder, expects:
//	objName - (string) main object name for the add-on, to be added to window element
//	objPathString - (string) add-on path name to use in URIs
//	addonUUID = (string) used to register the add-on's about: uri; must be unique for each add-on! See http://www.famkruithof.net/uuid/uuidgen for generating one
//	prefList - (object) { prefName: defaultValue } - add-on preferences
//	paneList - (iterable) of array with a panes arguments to apply to PrefPanes.setList(); see PrefPanes.jsm
//	addonUris - (object) containing any of the following string properties
//		homepage: add-on homepage
//		support: add-on support page
//		fullchangelog: add-on detailed changelog (usually github commits page)
//		email: developer e-mail
//		profile: developer profile or homepage
//		api: address from where it should fetch the data for the About pane's current development state
//		development: where the follow ongoing add-on development
//	startConditions(aData, aReason) -	(optional) (method) should return false if any requirements the add-on needs aren't met,
//						otherwise return true or call continueStartup(aData, aReason)
//	onStartup/onShutdown/onInstall/onUninstall(aData, aReason) - (optional) (methods) to be called appropriately as their name suggest
// handleDeadObject(ex) - 	expects [nsIScriptError object] ex. Shows dead object notices as warnings only in the console.
//				If the code can handle them accordingly and firefox does its thing, they shouldn't cause any problems.
// prepareObject(window, aName) - initializes a window-dependent add-on object with utils loaded into it, returns the newly created object
//	window - (xul object) the window object to be initialized
//	(optional) aName - (string) the object name, defaults to objName
// removeObject(window, aName) - closes and removes the object initialized by prepareObject()
//	see prepareObject()
// preparePreferences(window, aName) - loads the preferencesUtils module into that window's object initialized by prepareObject() (if it hasn't yet, it will be initialized)
//	see prepareObject()
// listenOnce(aSubject, type, handler, capture) - adds handler to window listening to event type that will be removed after one execution.
//	aSubject - (xul object) to add the handler to
//	type - (string) event type to listen to
//	handler - (function(event, aSubject)) - method to be called when event is triggered
//	(optional) capture - (bool) capture mode
// callOnLoad(aSubject, aCallback, beforeComplete) - calls aCallback immediately if aWindow is already loaded, otherwise waits for the load event to be fired on that window.
//	aSubject - (xul object) to execute aCallback on
//	aCallback - (function(aSubject)) to be called on aSubject
//	(optional) beforeComplete - (bool) if true, aCallback will be called on aSubject immediately, regardless of its readyState value; defaults to false.
// disable() - disables the add-on, in general the add-on disabling itself is a bad idea so I shouldn't use it

var UNLOADED = false;
var STARTED = false;
var Addon = {};
var AddonData = null;
var onceListeners = [];
var alwaysRunOnShutdown = [];
var MessengerLoaded = false;
var isChrome = true;
var isContent = false;

// Globals - lets me use objects that I can share through all the windows
var Globals = {};

// actual add-on data, to be overriden by defaults.js
var objName = null;
var objPathString = null;
var prefList = null;
var paneList = null;
var addonUUID = null;

// add-on relevant links, to be overriden by defaults.js
var addonUris = {
	homepage: '',
	support: '',
	fullchangelog: '',
	email: '',
	profile: '',
	api: '',
	development: ''
};

var {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");

// easy and useful helpers for when I'm debugging
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/Console.jsm");
function LOG(str) {
	if(!str) { str = typeof(str)+': '+str; }
	console.log(objName+' :: CHROME :: '+str);
}
function STEPLOGGER(name) {
	this.name = name;
	this.steps = [];
	this.initTime = new Date().getTime();
	this.lastTime = this.initTime;
}
STEPLOGGER.prototype = {
	step: function(name) {
		let time = new Date().getTime();
		this.steps.push({ name, time: time - this.lastTime});
		this.lastTime = time;
	},
	end: function() {
		this.step('end');
		let endTime = new Date().getTime();
		let report = { name: this.name, total: endTime - this.initTime };
		for(let x of this.steps) {
			report[x.name] = x.time;
		}
		console.log(report);
	}
};

XPCOMUtils.defineLazyServiceGetter(Services, "xulStore", "@mozilla.org/xul/xulstore;1", "nsIXULStore");
XPCOMUtils.defineLazyServiceGetter(Services, "navigator", "@mozilla.org/network/protocol;1?name=http", "nsIHttpProtocolHandler");
XPCOMUtils.defineLazyServiceGetter(Services, "stylesheet", "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService");

// I check these pretty much everywhere, so might as well keep a single reference to them
var WINNT = Services.appinfo.OS == 'WINNT';
var DARWIN = Services.appinfo.OS == 'Darwin';
var LINUX = !WINNT && !DARWIN;

function handleDeadObject(ex) {
	if(ex.message == "can't access dead object") {
		let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
		scriptError.init("Can't access dead object. This shouldn't cause any problems.", ex.sourceName || ex.fileName || null, ex.sourceLine || null, ex.lineNumber || null, ex.columnNumber || null, scriptError.warningFlag, 'XPConnect JavaScript');
		Services.console.logMessage(scriptError);
		return true;
	} else {
		Cu.reportError(ex);
		return false;
	}
}

function prepareObject(window, aName) {
	// I can override the object name if I want
	let objectName = aName || objName;
	if(window[objectName]) { return; }

	var rtl = getComputedStyle(window.document.documentElement).direction == 'rtl';

	window[objectName] = {
		objName: objectName,
		objPathString: objPathString,
		_UUID: new Date().getTime(),
		RTL: rtl,
		LTR: !rtl,

		// every supposedly global variable is inaccessible because bootstraped means sandboxed, so I have to reference all these;
		// it's easier to reference more specific objects from within the modules for better control, only setting these two here because they're more generalized
		window: window,
		get document () { return window.document; },
		$: function(id) { return window.document.getElementById(id); },
		$$: function(sel, parent = window.document) { return parent.querySelectorAll(sel); },
		$ª: function(parent, anonid, anonattr = 'anonid') { return window.document.getAnonymousElementByAttribute(parent, anonattr, anonid); }
	};

	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/Modules.jsm", window[objectName]);
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/windowUtilsPreload.jsm", window[objectName]);
	window[objectName].Modules.load("utils/windowUtils");

	setAttribute(window.document.documentElement, objectName+'_UUID', window[objectName]._UUID);
	setAttribute(window.document.documentElement, objectName+'_Version', AddonData.version);
}

function removeObject(window, aName) {
	let objectName = aName || objName;

	if(window[objectName]) {
		removeAttribute(window.document.documentElement, objectName+'_UUID');
		removeAttribute(window.document.documentElement, objectName+'_Version');
		window[objectName].Modules.unload("utils/windowUtils");
		delete window[objectName];
	}
}

function preparePreferences(window, aName) {
	let objectName = aName || objName;

	if(!window[objectName]) {
		prepareObject(window, objectName);
	}
	window[objectName].Modules.load("utils/preferencesUtils");
}

function removeOnceListener(oncer) {
	for(var i=0; i<onceListeners.length; i++) {
		if(!oncer) {
			onceListeners[i]();
			continue;
		}

		if(onceListeners[i] == oncer) {
			onceListeners.splice(i, 1);
			return;
		}
	}

	if(!oncer) {
		onceListeners = [];
	}
}

function listenOnce(aSubject, type, handler, capture) {
	if(UNLOADED || !aSubject || !aSubject.addEventListener) { return; }

	var runOnce = function(event) {
		try { aSubject.removeEventListener(type, runOnce, capture); }
		catch(ex) { handleDeadObject(ex); } // Prevents some can't access dead object errors
		if(!UNLOADED && event !== undefined) {
			removeOnceListener(runOnce);
			try { handler(event, aSubject); }
			catch(ex) { Cu.reportError(ex); }
		}
	};

	aSubject.addEventListener(type, runOnce, capture);
	onceListeners.push(runOnce);
}

function callOnLoad(aSubject, aCallback, beforeComplete) {
	if(aSubject.document.readyState == 'complete' || beforeComplete) {
		try { aCallback(aSubject); }
		catch(ex) { Cu.reportError(ex); }
		return;
	}

	// don't wait for the load event if we're terminating
	if(UNLOADED) { return; }

	listenOnce(aSubject, "load", function() {
		if(UNLOADED) { return; }

		try { aCallback(aSubject); }
		catch(ex) { Cu.reportError(ex); }
	}, false);
}

function disable() {
	AddonManager.getAddonByID(AddonData.id, function(addon) {
		addon.userDisabled = true;
	});
}

function continueStartup(aReason) {
	STARTED = aReason;

	// append actual preferences panes into the preferences tab
	if(paneList) {
		PrefPanes.setList(paneList);
	}

	if(typeof(onStartup) == 'function') {
		onStartup(AddonData, aReason);
	}
}

function startup(aData, aReason) {
	UNLOADED = false;
	AddonData = aData;

	// to make sure we get always the most recent files when updating the add-on, see:
	// https://bugzilla.mozilla.org/show_bug.cgi?id=918033
	// https://bugzilla.mozilla.org/show_bug.cgi?id=1051238
	AddonData.initTime = new Date().getTime();

	// This includes the optionsURL property
	AddonManager.getAddonByID(AddonData.id, function(addon) {
		if(typeof(UNLOADED) == 'undefined' || UNLOADED) { return; }
		Addon = addon;
	});

	// Set the default strings for the add-on
	let alias = Services.io.newFileURI(AddonData.installPath);
	let defaultsURI = ((AddonData.installPath.isDirectory()) ? alias.spec : 'jar:' + alias.spec + '!/')+'resource/defaults.js';
	Services.scriptloader.loadSubScript(defaultsURI, this);

	// Get the utils.jsm module into our sandbox
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/Modules.jsm", this);
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/sandboxUtilsPreload.jsm", this);
	Modules.load("utils/sandboxUtils");

	if(typeof(startConditions) != 'function' || startConditions(aReason)) {
		if(aReason == APP_STARTUP) {
			continueStartup(aReason);
		}
		// In non-e10s, loadFrameScript from this startup can run load content script even before the previous sandboxed content module had a chance to shutdown.
		// Case: enable add-on (instance A). Disable add-on. Re-enable add-on (instance B).
		// This has to happen in immediate succession, i.e. when updating add-on, but not only.
		// Sandbox content B tries to load before sandbox A received the shutdown message, so sandbox B never loads (because A is still loaded).
		// Then A receives the shutdown message, it shuts down, and we are left with nothing initialized in the child process.
		// Here's hoping that a delay of 500ms covers the vast majority of cases, edge-cases (ultra-busy CPU during add-on update?) may still suffer from this.
		// This doesn't seem to happen in e10s, but I'm leaving the delay there as well because why not. Maybe when e10s is norm this can be removed.
		else {
			aSync(function() {
				continueStartup(aReason);
			}, 500);
		}
	}
}

function shutdown(aData, aReason) {
	UNLOADED = aReason;

	if(aReason == APP_SHUTDOWN) {
		// List of methods that must always be run on shutdown, such as restoring some native prefs
		while(alwaysRunOnShutdown.length > 0) {
			alwaysRunOnShutdown.pop()();
		}

		removeOnceListener();
		return;
	}

	if(STARTED) {
		// content scripts should know as soon as possible that we're disabling the add-on, before any of them starts unloading,
		// otherwise they could try to load modules after the resource:// uri was gone
		if(MessengerLoaded) {
			Services.ppmm.broadcastAsyncMessage(Messenger.messageName('disable'));
		}

		if(typeof(onShutdown) == 'function') {
			onShutdown(aData, aReason);
		}
	}

	Modules.unload("utils/sandboxUtils");
	removeOnceListener();
}

function install(aData, aReason) {
	if(typeof(onInstall) == 'function') {
		onInstall(aData, aReason);
	}
}

function uninstall(aData, aReason) {
	if(typeof(onUninstall) == 'function') {
		onUninstall(aData, aReason);
	}
}
<commit_msg>fix memory leak on chrome tabs<commit_after>// VERSION 1.8.10

// This looks for file defaults.js in resource folder, expects:
//	objName - (string) main object name for the add-on, to be added to window element
//	objPathString - (string) add-on path name to use in URIs
//	addonUUID = (string) used to register the add-on's about: uri; must be unique for each add-on! See http://www.famkruithof.net/uuid/uuidgen for generating one
//	prefList - (object) { prefName: defaultValue } - add-on preferences
//	paneList - (iterable) of array with a panes arguments to apply to PrefPanes.setList(); see PrefPanes.jsm
//	addonUris - (object) containing any of the following string properties
//		homepage: add-on homepage
//		support: add-on support page
//		fullchangelog: add-on detailed changelog (usually github commits page)
//		email: developer e-mail
//		profile: developer profile or homepage
//		api: address from where it should fetch the data for the About pane's current development state
//		development: where the follow ongoing add-on development
//	startConditions(aData, aReason) -	(optional) (method) should return false if any requirements the add-on needs aren't met,
//						otherwise return true or call continueStartup(aData, aReason)
//	onStartup/onShutdown/onInstall/onUninstall(aData, aReason) - (optional) (methods) to be called appropriately as their name suggest
// handleDeadObject(ex) - 	expects [nsIScriptError object] ex. Shows dead object notices as warnings only in the console.
//				If the code can handle them accordingly and firefox does its thing, they shouldn't cause any problems.
// prepareObject(window, aName) - initializes a window-dependent add-on object with utils loaded into it, returns the newly created object
//	window - (xul object) the window object to be initialized
//	(optional) aName - (string) the object name, defaults to objName
// removeObject(window, aName) - closes and removes the object initialized by prepareObject()
//	see prepareObject()
// preparePreferences(window, aName) - loads the preferencesUtils module into that window's object initialized by prepareObject() (if it hasn't yet, it will be initialized)
//	see prepareObject()
// listenOnce(aSubject, type, handler, capture) - adds handler to window listening to event type that will be removed after one execution.
//	aSubject - (xul object) to add the handler to
//	type - (string) event type to listen to
//	handler - (function(event, aSubject)) - method to be called when event is triggered
//	(optional) capture - (bool) capture mode
// callOnLoad(aSubject, aCallback, beforeComplete) - calls aCallback immediately if aWindow is already loaded, otherwise waits for the load event to be fired on that window.
//	aSubject - (xul object) to execute aCallback on
//	aCallback - (function(aSubject)) to be called on aSubject
//	(optional) beforeComplete - (bool) if true, aCallback will be called on aSubject immediately, regardless of its readyState value; defaults to false.
// disable() - disables the add-on, in general the add-on disabling itself is a bad idea so I shouldn't use it

var UNLOADED = false;
var STARTED = false;
var Addon = {};
var AddonData = null;
var onceListeners = [];
var alwaysRunOnShutdown = [];
var MessengerLoaded = false;
var isChrome = true;
var isContent = false;

// Globals - lets me use objects that I can share through all the windows
var Globals = {};

// actual add-on data, to be overriden by defaults.js
var objName = null;
var objPathString = null;
var prefList = null;
var paneList = null;
var addonUUID = null;

// add-on relevant links, to be overriden by defaults.js
var addonUris = {
	homepage: '',
	support: '',
	fullchangelog: '',
	email: '',
	profile: '',
	api: '',
	development: ''
};

var {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");

// easy and useful helpers for when I'm debugging
XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/Console.jsm");
function LOG(str) {
	if(!str) { str = typeof(str)+': '+str; }
	console.log(objName+' :: CHROME :: '+str);
}
function STEPLOGGER(name) {
	this.name = name;
	this.steps = [];
	this.initTime = new Date().getTime();
	this.lastTime = this.initTime;
}
STEPLOGGER.prototype = {
	step: function(name) {
		let time = new Date().getTime();
		this.steps.push({ name, time: time - this.lastTime});
		this.lastTime = time;
	},
	end: function() {
		this.step('end');
		let endTime = new Date().getTime();
		let report = { name: this.name, total: endTime - this.initTime };
		for(let x of this.steps) {
			report[x.name] = x.time;
		}
		console.log(report);
	}
};

XPCOMUtils.defineLazyServiceGetter(Services, "xulStore", "@mozilla.org/xul/xulstore;1", "nsIXULStore");
XPCOMUtils.defineLazyServiceGetter(Services, "navigator", "@mozilla.org/network/protocol;1?name=http", "nsIHttpProtocolHandler");
XPCOMUtils.defineLazyServiceGetter(Services, "stylesheet", "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService");

// I check these pretty much everywhere, so might as well keep a single reference to them
var WINNT = Services.appinfo.OS == 'WINNT';
var DARWIN = Services.appinfo.OS == 'Darwin';
var LINUX = !WINNT && !DARWIN;

function handleDeadObject(ex) {
	if(ex.message == "can't access dead object") {
		let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
		scriptError.init("Can't access dead object. This shouldn't cause any problems.", ex.sourceName || ex.fileName || null, ex.sourceLine || null, ex.lineNumber || null, ex.columnNumber || null, scriptError.warningFlag, 'XPConnect JavaScript');
		Services.console.logMessage(scriptError);
		return true;
	} else {
		Cu.reportError(ex);
		return false;
	}
}

function prepareObject(window, aName) {
	// I can override the object name if I want
	let objectName = aName || objName;
	if(window[objectName]) { return; }

	var rtl = getComputedStyle(window.document.documentElement).direction == 'rtl';

	window[objectName] = {
		objName: objectName,
		objPathString: objPathString,
		_UUID: new Date().getTime(),
		RTL: rtl,
		LTR: !rtl,

		// every supposedly global variable is inaccessible because bootstraped means sandboxed, so I have to reference all these;
		// it's easier to reference more specific objects from within the modules for better control, only setting these two here because they're more generalized
		window: window,
		get document () { return window.document; },
		$: function(id) { return window.document.getElementById(id); },
		$$: function(sel, parent = window.document) { return parent.querySelectorAll(sel); },
		$ª: function(parent, anonid, anonattr = 'anonid') { return window.document.getAnonymousElementByAttribute(parent, anonattr, anonid); }
	};

	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/Modules.jsm", window[objectName]);
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/windowUtilsPreload.jsm", window[objectName]);
	window[objectName].Modules.load("utils/windowUtils");

	setAttribute(window.document.documentElement, objectName+'_UUID', window[objectName]._UUID);
	setAttribute(window.document.documentElement, objectName+'_Version', AddonData.version);
}

function removeObject(window, aName) {
	let objectName = aName || objName;

	if(window[objectName]) {
		removeAttribute(window.document.documentElement, objectName+'_UUID');
		removeAttribute(window.document.documentElement, objectName+'_Version');
		window[objectName].Modules.unload("utils/windowUtils");
		delete window[objectName];
	}
}

function preparePreferences(window, aName) {
	let objectName = aName || objName;

	if(!window[objectName]) {
		prepareObject(window, objectName);
	}
	window[objectName].Modules.load("utils/preferencesUtils");
}

function removeOnceListener(oncer) {
	for(var i=0; i<onceListeners.length; i++) {
		let unwrapped = onceListeners[i].get();

		// clean up dead weak ref
		if(!unwrapped) {
			onceListeners.splice(i, 1);
			i--;
			continue;
		}

		if(!oncer) {
			unwrapped();
			continue;
		}

		if(unwrapped == oncer) {
			onceListeners.splice(i, 1);
			return;
		}
	}

	if(!oncer) {
		onceListeners = [];
	}
}

function listenOnce(aSubject, type, handler, capture) {
	if(UNLOADED || !aSubject || !aSubject.addEventListener) { return; }

	var runOnce = function(event) {
		try { aSubject.removeEventListener(type, runOnce, capture); }
		catch(ex) { handleDeadObject(ex); } // Prevents some can't access dead object errors
		if(!UNLOADED && event !== undefined) {
			removeOnceListener(runOnce);
			try { handler(event, aSubject); }
			catch(ex) { Cu.reportError(ex); }
		}
	};

	aSubject.addEventListener(type, runOnce, capture);

	let weak = Cu.getWeakReference(runOnce);
	onceListeners.push(weak);
}

function callOnLoad(aSubject, aCallback, beforeComplete) {
	if(aSubject.document.readyState == 'complete' || beforeComplete) {
		try { aCallback(aSubject); }
		catch(ex) { Cu.reportError(ex); }
		return;
	}

	// don't wait for the load event if we're terminating
	if(UNLOADED) { return; }

	listenOnce(aSubject, "load", function() {
		if(UNLOADED) { return; }

		try { aCallback(aSubject); }
		catch(ex) { Cu.reportError(ex); }
	}, false);
}

function disable() {
	AddonManager.getAddonByID(AddonData.id, function(addon) {
		addon.userDisabled = true;
	});
}

function continueStartup(aReason) {
	STARTED = aReason;

	// append actual preferences panes into the preferences tab
	if(paneList) {
		PrefPanes.setList(paneList);
	}

	if(typeof(onStartup) == 'function') {
		onStartup(AddonData, aReason);
	}
}

function startup(aData, aReason) {
	UNLOADED = false;
	AddonData = aData;

	// to make sure we get always the most recent files when updating the add-on, see:
	// https://bugzilla.mozilla.org/show_bug.cgi?id=918033
	// https://bugzilla.mozilla.org/show_bug.cgi?id=1051238
	AddonData.initTime = new Date().getTime();

	// This includes the optionsURL property
	AddonManager.getAddonByID(AddonData.id, function(addon) {
		if(typeof(UNLOADED) == 'undefined' || UNLOADED) { return; }
		Addon = addon;
	});

	// Set the default strings for the add-on
	let alias = Services.io.newFileURI(AddonData.installPath);
	let defaultsURI = ((AddonData.installPath.isDirectory()) ? alias.spec : 'jar:' + alias.spec + '!/')+'resource/defaults.js';
	Services.scriptloader.loadSubScript(defaultsURI, this);

	// Get the utils.jsm module into our sandbox
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/Modules.jsm", this);
	Services.scriptloader.loadSubScript("resource://"+objPathString+"/modules/utils/sandboxUtilsPreload.jsm", this);
	Modules.load("utils/sandboxUtils");

	if(typeof(startConditions) != 'function' || startConditions(aReason)) {
		if(aReason == APP_STARTUP) {
			continueStartup(aReason);
		}
		// In non-e10s, loadFrameScript from this startup can run load content script even before the previous sandboxed content module had a chance to shutdown.
		// Case: enable add-on (instance A). Disable add-on. Re-enable add-on (instance B).
		// This has to happen in immediate succession, i.e. when updating add-on, but not only.
		// Sandbox content B tries to load before sandbox A received the shutdown message, so sandbox B never loads (because A is still loaded).
		// Then A receives the shutdown message, it shuts down, and we are left with nothing initialized in the child process.
		// Here's hoping that a delay of 500ms covers the vast majority of cases, edge-cases (ultra-busy CPU during add-on update?) may still suffer from this.
		// This doesn't seem to happen in e10s, but I'm leaving the delay there as well because why not. Maybe when e10s is norm this can be removed.
		else {
			aSync(function() {
				continueStartup(aReason);
			}, 500);
		}
	}
}

function shutdown(aData, aReason) {
	UNLOADED = aReason;

	if(aReason == APP_SHUTDOWN) {
		// List of methods that must always be run on shutdown, such as restoring some native prefs
		while(alwaysRunOnShutdown.length > 0) {
			alwaysRunOnShutdown.pop()();
		}

		removeOnceListener();
		return;
	}

	if(STARTED) {
		// content scripts should know as soon as possible that we're disabling the add-on, before any of them starts unloading,
		// otherwise they could try to load modules after the resource:// uri was gone
		if(MessengerLoaded) {
			Services.ppmm.broadcastAsyncMessage(Messenger.messageName('disable'));
		}

		if(typeof(onShutdown) == 'function') {
			onShutdown(aData, aReason);
		}
	}

	Modules.unload("utils/sandboxUtils");
	removeOnceListener();
}

function install(aData, aReason) {
	if(typeof(onInstall) == 'function') {
		onInstall(aData, aReason);
	}
}

function uninstall(aData, aReason) {
	if(typeof(onUninstall) == 'function') {
		onUninstall(aData, aReason);
	}
}
<|endoftext|>


<commit_before>/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var fs = require('fs');
var exec = require('child_process').exec;

function main(RSVP, name) {
  function replaceStringInFile(path, find, replace) {
    return new RSVP.Promise(function (resolve, reject) {
      fs.readFile(__dirname + '/' + path, 'utf8', function(error, data) {
        if (error) {
          reject('Error reading file ' + __dirname + '/' + path + ': ' + error);
        }

        var replacedFile = data.replace(find, replace);

        fs.writeFile(__dirname + '/' + path, replacedFile, function(error) {
          if (error) {
            reject('Error writing file ' + __dirname + '/' + path + ': ' + error);
          } else {
            resolve();
          }
        });
      });
    });
  }

  return RSVP.all([
    replaceStringInFile('./js/app.js', /<YOUR-FIREBASE-APP>/g, name),
    replaceStringInFile('./html/index.html', /<YOUR-FIREBASE-APP>/g, name),
    replaceStringInFile('./bower.json', /<YOUR-FIREBASE-APP>/g, name)
  ]);

  // npm install bower (if not exists)
  //exec("npm install -g bower", function () {console.log(arguments)});

  // bower install
  //exec("bower install", function () {console.log(arguments)});
}

module.exports = {
  setup: main
}
<commit_msg>Improve bootstrap.js<commit_after>/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var fs = require('fs');
var exec = require('child_process').exec;
var os = require('os');

function main(RSVP, name) {
  /* Utility Methods */
  function replaceStringInFile(path, find, replace) {
    return new RSVP.Promise(function (resolve, reject) {
      fs.readFile(__dirname + '/' + path, 'utf8', function(error, data) {
        if (error) {
          reject('Error reading file ' + __dirname + '/' + path + ': ' + error);
        }

        var replacedFile = data.replace(find, replace);

        fs.writeFile(__dirname + '/' + path, replacedFile, function(error) {
          if (error) {
            reject('Error writing file ' + __dirname + '/' + path + ': ' + error);
          } else {
            resolve();
          }
        });
      });
    });
  }

  function promisedExec(command) {
    return new RSVP.Promise(function (resolve, reject) {
      exec(command, function (err, stdout, stderr) {
        if (err) reject(err);
        else resolve(stdout);
      });
    });
  }

  /* Repo Specific Methods */
  function checkForBower() {
    console.log("Looking for local Bower...");
    return new RSVP.Promise(function (resolve, reject) {
      exec("bower -v", function (err, stdout, stderr) {
        if (err) {
          console.log("Temporarily install Bower...");
          promisedExec("npm install bower --prefix " + os.tmpdir() + "/fb_bootstrap")
            .then(resolve.bind(resolve, "temporary"))
            .catch(reject);
        } else {
          resolve("local");
        }
      })
    });
  }

  function installBowerDependencies(location) {
    console.log("Installing Bower dependecies...");
    var command;

    if (location == "temporary") {
      command = os.tmpdir() + "/fb_bootstrap/node_modules/.bin/bower install";
    } else {
      command = "bower install";
    }

    return promisedExec(command);
  }

  function renameToFirebaseApp() {
    console.log("Renaming application...");
    return RSVP.all(['./js/app.js', './html/index.html', './bower.json'].map(function (file) {
      return replaceStringInFile(file, /<YOUR-FIREBASE-APP>/g, name);
    }));
  }

  /* Bootstrap */
  return checkForBower()
    .then(installBowerDependencies)
    .then(renameToFirebaseApp)
    .then(function () {
      console.log("Bootstrapped!")
    })
    .catch(function (err) {
      console.log("Something went wrong :(", err)
    });
}

function getNodeModule(moduleName, cb) {
  exec("npm install " + moduleName + " --prefix " + os.tmpdir() + "/fb_bootstrap", function (err, stdout, stderr) {
    if (err)
      cb(err);
    else
      cb(null, require(os.tmpdir() + "/fb_bootstrap/node_modules/" + moduleName));
  });
}

module.exports = {
  setup: main
}

if (require.main === module) {
  getNodeModule("rsvp", function (err, RSVP) {
    if (err) throw new Error(err);
    else main.apply(this, [RSVP].concat(process.argv.slice(2)))
  });
}
<|endoftext|>


<commit_before>define([
    '/bower_components/tweetnacl/nacl-fast.min.js',
], function () {
    var Nacl = window.nacl;
    //var PARANOIA = true;

    var plainChunkLength = 128 * 1024;
    var cypherChunkLength = 131088;

    var computeEncryptedSize = function (bytes, meta) {
        var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length;
        var chunks = Math.ceil(bytes / plainChunkLength);
        return metasize + 18 + (chunks * 16) + bytes;
    };

    var encodePrefix = function (p) {
        return [
            65280, // 255 << 8
            255,
        ].map(function (n, i) {
            return (p & n) >> ((1 - i) * 8);
        });
    };
    var decodePrefix = function (A) {
        return (A[0] << 8) | A[1];
    };

    var slice = function (A) {
        return Array.prototype.slice.call(A);
    };

    var createNonce = function () {
        return new Uint8Array(new Array(24).fill(0));
    };

    var increment = function (N) {
        var l = N.length;
        while (l-- > 1) {
        /*  jshint probably suspects this is unsafe because we lack types
            but as long as this is only used on nonces, it should be safe  */
            if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
            if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); }
            N[l] = 0;
        }
    };

    var joinChunks = function (chunks) {
        return new Blob(chunks);
    };

    var concatBuffer = function (a, b) { // TODO make this not so ugly
        return new Uint8Array(slice(a).concat(slice(b)));
    };

    var fetchMetadata = function (src, cb) {
        var done = false;
        var CB = function (err, res) {
            if (done) { return; }
            done = true;
            cb(err, res);
        };

        var xhr = new XMLHttpRequest();
        xhr.open("GET", src, true);
        xhr.setRequestHeader('Range', 'bytes=0-1');
        xhr.responseType = 'arraybuffer';

        xhr.onerror= function () { return CB('XHR_ERROR'); };
        xhr.onload = function () {
            if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
            var res = new Uint8Array(xhr.response);
            var size = decodePrefix(res);
            var xhr2 = new XMLHttpRequest();

            xhr2.open("GET", src, true);
            xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2));
            xhr2.responseType = 'arraybuffer';
            xhr2.onload = function () {
                if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
                var res2 = new Uint8Array(xhr2.response);
                var all = concatBuffer(res, res2);
                CB(void 0, all);
            };
            xhr2.send(null);
        };
        xhr.send(null);
    };

    var decryptMetadata = function (u8, key) {
        var prefix = u8.subarray(0, 2);
        var metadataLength = decodePrefix(prefix);

        var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
        var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key);

        try {
            return JSON.parse(Nacl.util.encodeUTF8(metaChunk));
        }
        catch (e) { return null; }
    };

    var fetchDecryptedMetadata = function (src, key, cb) {
        if (typeof(src) !== 'string') {
            return window.setTimeout(function () {
                cb('NO_SOURCE');
            });
        }
        fetchMetadata(src, function (e, buffer) {
            if (e) { return cb(e); }
            cb(void 0, decryptMetadata(buffer, key));
        });
    };

    var decrypt = function (u8, key, done, progress) {
        var MAX = u8.length;
        var _progress = function (offset) {
            if (typeof(progress) !== 'function') { return; }
            progress(Math.min(1, offset / MAX));
        };

        var nonce = createNonce();
        var i = 0;

        var prefix = u8.subarray(0, 2);
        var metadataLength = decodePrefix(prefix);

        var res = {
            metadata: undefined,
        };

        var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));

        var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
        increment(nonce);

        try {
            res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk));
        } catch (e) {
            return window.setTimeout(function () {
                done('E_METADATA_DECRYPTION');
            });
        }

        if (!res.metadata) {
            return void setTimeout(function () {
                done('NO_METADATA');
            });
        }

        var takeChunk = function (cb) {
            setTimeout(function () {
                var start = i * cypherChunkLength + 2 + metadataLength;
                var end = start + cypherChunkLength;
                i++;
                var box = new Uint8Array(u8.subarray(start, end));

                // decrypt the chunk
                var plaintext = Nacl.secretbox.open(box, nonce, key);
                increment(nonce);

                if (!plaintext) { return cb('DECRYPTION_ERROR'); }

                _progress(end);
                cb(void 0, plaintext);
            });
        };

        var chunks = [];

        var again = function () {
            takeChunk(function (e, plaintext) {
                if (e) {
                    return setTimeout(function () {
                        done(e);
                    });
                }
                if (plaintext) {
                    if ((2 + metadataLength + i * cypherChunkLength) < u8.length) { // not done
                        chunks.push(plaintext);
                        return setTimeout(again);
                    }
                    chunks.push(plaintext);
                    res.content = joinChunks(chunks);
                    return done(void 0, res);
                }
                done('UNEXPECTED_ENDING');
            });
        };

        again();
    };

    // metadata
    /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */
    var encrypt = function (u8, metadata, key) {
        var nonce = createNonce();

        // encode metadata
        var plaintext = Nacl.util.decodeUTF8(JSON.stringify(metadata));

        // if metadata is too large, drop the thumbnail.
        if (plaintext.length > 65535) {
            var temp = JSON.parse(JSON.stringify(metadata));
            delete metadata.thumbnail;
            plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp));
        }

        var i = 0;

        var state = 0;
        var next = function (cb) {
            if (state === 2) { return void setTimeout(cb); }

            var start;
            var end;
            var part;
            var box;

            if (state === 0) { // metadata...
                part = new Uint8Array(plaintext);
                box = Nacl.secretbox(part, nonce, key);
                increment(nonce);

                if (box.length > 65535) {
                    return void cb('METADATA_TOO_LARGE');
                }
                var prefixed = new Uint8Array(encodePrefix(box.length)
                    .concat(slice(box)));
                state++;

                return void setTimeout(function () {
                    cb(void 0, prefixed);
                });
            }

            // encrypt the rest of the file...
            start = i * plainChunkLength;
            end = start + plainChunkLength;

            part = u8.subarray(start, end);
            box = Nacl.secretbox(part, nonce, key);
            increment(nonce);
            i++;

            // regular data is done
            if (i * plainChunkLength >= u8.length) { state = 2; }

            setTimeout(function () {
                cb(void 0, box);
            });
        };

        return next;
    };

    return {
        decrypt: decrypt,
        encrypt: encrypt,
        joinChunks: joinChunks,
        computeEncryptedSize: computeEncryptedSize,
        decryptMetadata: decryptMetadata,
        fetchMetadata: fetchMetadata,
        fetchDecryptedMetadata: fetchDecryptedMetadata,
    };
});
<commit_msg>address a linter error<commit_after>define([
    '/bower_components/tweetnacl/nacl-fast.min.js',
], function () {
    var Nacl = window.nacl;
    //var PARANOIA = true;

    var plainChunkLength = 128 * 1024;
    var cypherChunkLength = 131088;

    var computeEncryptedSize = function (bytes, meta) {
        var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length;
        var chunks = Math.ceil(bytes / plainChunkLength);
        return metasize + 18 + (chunks * 16) + bytes;
    };

    var encodePrefix = function (p) {
        return [
            65280, // 255 << 8
            255,
        ].map(function (n, i) {
            return (p & n) >> ((1 - i) * 8);
        });
    };
    var decodePrefix = function (A) {
        return (A[0] << 8) | A[1];
    };

    var slice = function (A) {
        return Array.prototype.slice.call(A);
    };

    var createNonce = function () {
        return new Uint8Array(new Array(24).fill(0));
    };

    var increment = function (N) {
        var l = N.length;
        while (l-- > 1) {
        /*  our linter suspects this is unsafe because we lack types
            but as long as this is only used on nonces, it should be safe  */
            if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
            if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); }
            N[l] = 0;
        }
    };

    var joinChunks = function (chunks) {
        return new Blob(chunks);
    };

    var concatBuffer = function (a, b) { // TODO make this not so ugly
        return new Uint8Array(slice(a).concat(slice(b)));
    };

    var fetchMetadata = function (src, cb) {
        var done = false;
        var CB = function (err, res) {
            if (done) { return; }
            done = true;
            cb(err, res);
        };

        var xhr = new XMLHttpRequest();
        xhr.open("GET", src, true);
        xhr.setRequestHeader('Range', 'bytes=0-1');
        xhr.responseType = 'arraybuffer';

        xhr.onerror= function () { return CB('XHR_ERROR'); };
        xhr.onload = function () {
            if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
            var res = new Uint8Array(xhr.response);
            var size = decodePrefix(res);
            var xhr2 = new XMLHttpRequest();

            xhr2.open("GET", src, true);
            xhr2.setRequestHeader('Range', 'bytes=2-' + (size + 2));
            xhr2.responseType = 'arraybuffer';
            xhr2.onload = function () {
                if (/^4/.test('' + this.status)) { return CB('XHR_ERROR'); }
                var res2 = new Uint8Array(xhr2.response);
                var all = concatBuffer(res, res2);
                CB(void 0, all);
            };
            xhr2.send(null);
        };
        xhr.send(null);
    };

    var decryptMetadata = function (u8, key) {
        var prefix = u8.subarray(0, 2);
        var metadataLength = decodePrefix(prefix);

        var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
        var metaChunk = Nacl.secretbox.open(metaBox, createNonce(), key);

        try {
            return JSON.parse(Nacl.util.encodeUTF8(metaChunk));
        }
        catch (e) { return null; }
    };

    var fetchDecryptedMetadata = function (src, key, cb) {
        if (typeof(src) !== 'string') {
            return window.setTimeout(function () {
                cb('NO_SOURCE');
            });
        }
        fetchMetadata(src, function (e, buffer) {
            if (e) { return cb(e); }
            cb(void 0, decryptMetadata(buffer, key));
        });
    };

    var decrypt = function (u8, key, done, progress) {
        var MAX = u8.length;
        var _progress = function (offset) {
            if (typeof(progress) !== 'function') { return; }
            progress(Math.min(1, offset / MAX));
        };

        var nonce = createNonce();
        var i = 0;

        var prefix = u8.subarray(0, 2);
        var metadataLength = decodePrefix(prefix);

        var res = {
            metadata: undefined,
        };

        var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));

        var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
        increment(nonce);

        try {
            res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk));
        } catch (e) {
            return window.setTimeout(function () {
                done('E_METADATA_DECRYPTION');
            });
        }

        if (!res.metadata) {
            return void setTimeout(function () {
                done('NO_METADATA');
            });
        }

        var takeChunk = function (cb) {
            setTimeout(function () {
                var start = i * cypherChunkLength + 2 + metadataLength;
                var end = start + cypherChunkLength;
                i++;
                var box = new Uint8Array(u8.subarray(start, end));

                // decrypt the chunk
                var plaintext = Nacl.secretbox.open(box, nonce, key);
                increment(nonce);

                if (!plaintext) { return cb('DECRYPTION_ERROR'); }

                _progress(end);
                cb(void 0, plaintext);
            });
        };

        var chunks = [];

        var again = function () {
            takeChunk(function (e, plaintext) {
                if (e) {
                    return setTimeout(function () {
                        done(e);
                    });
                }
                if (plaintext) {
                    if ((2 + metadataLength + i * cypherChunkLength) < u8.length) { // not done
                        chunks.push(plaintext);
                        return setTimeout(again);
                    }
                    chunks.push(plaintext);
                    res.content = joinChunks(chunks);
                    return done(void 0, res);
                }
                done('UNEXPECTED_ENDING');
            });
        };

        again();
    };

    // metadata
    /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */
    var encrypt = function (u8, metadata, key) {
        var nonce = createNonce();

        // encode metadata
        var plaintext = Nacl.util.decodeUTF8(JSON.stringify(metadata));

        // if metadata is too large, drop the thumbnail.
        if (plaintext.length > 65535) {
            var temp = JSON.parse(JSON.stringify(metadata));
            delete metadata.thumbnail;
            plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp));
        }

        var i = 0;

        var state = 0;
        var next = function (cb) {
            if (state === 2) { return void setTimeout(cb); }

            var start;
            var end;
            var part;
            var box;

            if (state === 0) { // metadata...
                part = new Uint8Array(plaintext);
                box = Nacl.secretbox(part, nonce, key);
                increment(nonce);

                if (box.length > 65535) {
                    return void cb('METADATA_TOO_LARGE');
                }
                var prefixed = new Uint8Array(encodePrefix(box.length)
                    .concat(slice(box)));
                state++;

                return void setTimeout(function () {
                    cb(void 0, prefixed);
                });
            }

            // encrypt the rest of the file...
            start = i * plainChunkLength;
            end = start + plainChunkLength;

            part = u8.subarray(start, end);
            box = Nacl.secretbox(part, nonce, key);
            increment(nonce);
            i++;

            // regular data is done
            if (i * plainChunkLength >= u8.length) { state = 2; }

            setTimeout(function () {
                cb(void 0, box);
            });
        };

        return next;
    };

    return {
        decrypt: decrypt,
        encrypt: encrypt,
        joinChunks: joinChunks,
        computeEncryptedSize: computeEncryptedSize,
        decryptMetadata: decryptMetadata,
        fetchMetadata: fetchMetadata,
        fetchDecryptedMetadata: fetchDecryptedMetadata,
    };
});
<|endoftext|>