<commit_before>'use strict';

const retrieveBSON = require('./connection/utils').retrieveBSON;
const EventEmitter = require('events');
const BSON = retrieveBSON();
const Binary = BSON.Binary;
const uuidV4 = require('./utils').uuidV4;
const MongoError = require('./error').MongoError;
const isRetryableError = require('././error').isRetryableError;

function assertAlive(session, callback) {
  if (session.serverSession == null) {
    const error = new MongoError('Cannot use a session that has ended');
    if (typeof callback === 'function') {
      callback(error, null);
      return false;
    }

    throw error;
  }

  return true;
}

/** A class representing a client session on the server */
class ClientSession extends EventEmitter {
  /**
   * Create a client session.
   * WARNING: not meant to be instantiated directly
   *
   * @param {Topology} topology The current client's topology
   * @param {ServerSessionPool} sessionPool The server session pool
   * @param {Object} [options] Optional settings
   * @param {Boolean} [options.causalConsistency] Whether causal consistency should be enabled on this session
   * @param {Boolean} [options.autoStartTransaction=false] When enabled this session automatically starts a transaction with the provided defaultTransactionOptions.
   * @param {Object} [options.defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
   * @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
   */
  constructor(topology, sessionPool, options, clientOptions) {
    super();

    if (topology == null) {
      throw new Error('ClientSession requires a topology');
    }

    if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
      throw new Error('ClientSession requires a ServerSessionPool');
    }

    options = options || {};
    this.topology = topology;
    this.sessionPool = sessionPool;
    this.hasEnded = false;
    this.serverSession = sessionPool.acquire();
    this.clientOptions = clientOptions;

    this.supports = {
      causalConsistency:
        typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
    };

    options = options || {};
    if (typeof options.initialClusterTime !== 'undefined') {
      this.clusterTime = options.initialClusterTime;
    } else {
      this.clusterTime = null;
    }

    this.operationTime = null;
    this.explicit = !!options.explicit;
    this.owner = options.owner;
    this.transactionOptions = null;
    this.autoStartTransaction = options.autoStartTransaction;
    this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
  }

  /**
   * Ends this session on the server
   *
   * @param {Object} [options] Optional settings
   * @param {Function} [callback] Optional callback for completion of this operation
   */
  endSession(options, callback) {
    if (typeof options === 'function') (callback = options), (options = {});
    options = options || {};

    if (this.hasEnded) {
      if (typeof callback === 'function') callback(null, null);
      return;
    }

    if (this.serverSession && this.inTransaction()) {
      this.abortTransaction(); // pass in callback?
    }

    // mark the session as ended, and emit a signal
    this.hasEnded = true;
    this.emit('ended', this);

    // release the server session back to the pool
    this.sessionPool.release(this.serverSession);

    // spec indicates that we should ignore all errors for `endSessions`
    if (typeof callback === 'function') callback(null, null);
  }

  /**
   * Advances the operationTime for a ClientSession.
   *
   * @param {object} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
   */
  advanceOperationTime(operationTime) {
    if (this.operationTime == null) {
      this.operationTime = operationTime;
      return;
    }

    if (operationTime.greaterThan(this.operationTime)) {
      this.operationTime = operationTime;
    }
  }

  /**
   * Used to determine if this session equals another
   */
  equals(session) {
    if (!(session instanceof ClientSession)) {
      return false;
    }

    return this.id.id.buffer.equals(session.id.id.buffer);
  }

  /**
   * Increment the transaction number on the internal ServerSession
   */
  incrementTransactionNumber() {
    this.serverSession.txnNumber++;
  }

  /**
   * @returns whether this session is current in a transaction or not
   */
  inTransaction() {
    return this.transactionOptions != null;
  }

  /**
   * Starts a new transaction with the given options.
   *
   * @param {Object} options Optional settings
   * @param {ReadConcern} [options.readConcern] The readConcern to use for this transaction
   * @param {WriteConcern} [options.writeConcern] The writeConcern to use for this transaction
   */
  startTransaction(options) {
    assertAlive(this);
    if (this.inTransaction()) {
      throw new MongoError('Transaction already in progress');
    }

    // increment txnNumber and reset stmtId to zero.
    this.serverSession.txnNumber += 1;

    // set transaction options, we will use this to determine if we are in a transaction
    this.transactionOptions = Object.assign({}, options || this.defaultTransactionOptions);
  }

  /**
   * Commits the currently active transaction in this session.
   *
   * @param {Function} [callback] optional callback for completion of this operation
   * @return {Promise} A promise is returned if no callback is provided
   */
  commitTransaction(callback) {
    if (typeof callback === 'function') {
      endTransaction(this, 'commitTransaction', callback);
      return;
    }

    return new Promise((resolve, reject) => {
      endTransaction(
        this,
        'commitTransaction',
        (err, reply) => (err ? reject(err) : resolve(reply))
      );
    });
  }

  /**
   * Aborts the currently active transaction in this session.
   *
   * @param {Function} [callback] optional callback for completion of this operation
   * @return {Promise} A promise is returned if no callback is provided
   */
  abortTransaction(callback) {
    if (typeof callback === 'function') {
      endTransaction(this, 'abortTransaction', callback);
      return;
    }

    return new Promise((resolve, reject) => {
      endTransaction(
        this,
        'abortTransaction',
        (err, reply) => (err ? reject(err) : resolve(reply))
      );
    });
  }
}

function resetTransactionState(clientSession) {
  clientSession.transactionOptions = null;
}

function endTransaction(clientSession, commandName, callback) {
  if (!assertAlive(clientSession, callback)) {
    // checking result in case callback was called
    return;
  }

  if (!clientSession.inTransaction()) {
    if (clientSession.autoStartTransaction) {
      clientSession.startTransaction();
    } else {
      callback(new MongoError('No transaction started'));
      return;
    }
  }

  if (clientSession.serverSession.stmtId === 0) {
    // The server transaction was never started.
    resetTransactionState(clientSession);
    callback(null, null);
    return;
  }

  const command = { [commandName]: 1 };
  if (clientSession.transactionOptions.writeConcern) {
    Object.assign(command, { writeConcern: clientSession.transactionOptions.writeConcern });
  } else if (clientSession.clientOptions && clientSession.clientOptions.w) {
    Object.assign(command, { writeConcern: { w: clientSession.clientOptions.w } });
  }

  function commandHandler(e, r) {
    resetTransactionState(clientSession);
    callback(e, r);
  }

  function transactionError(err) {
    return commandName === 'commitTransaction' ? err : null;
  }

  // send the command
  clientSession.topology.command(
    'admin.$cmd',
    command,
    { session: clientSession },
    (err, reply) => {
      if (err && isRetryableError(err)) {
        return clientSession.topology.command(
          'admin.$cmd',
          command,
          { session: clientSession },
          (_err, _reply) => commandHandler(transactionError(_err), _reply)
        );
      }

      commandHandler(transactionError(err), reply);
    }
  );
}

Object.defineProperty(ClientSession.prototype, 'id', {
  get: function() {
    return this.serverSession.id;
  }
});

/**
 *
 */
class ServerSession {
  constructor() {
    this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
    this.lastUse = Date.now();
    this.txnNumber = 0;
  }

  /**
   *
   * @param {*} sessionTimeoutMinutes
   */
  hasTimedOut(sessionTimeoutMinutes) {
    // Take the difference of the lastUse timestamp and now, which will result in a value in
    // milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
    const idleTimeMinutes = Math.round(
      (((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
    );

    return idleTimeMinutes > sessionTimeoutMinutes - 1;
  }
}

/**
 *
 */
class ServerSessionPool {
  constructor(topology) {
    if (topology == null) {
      throw new Error('ServerSessionPool requires a topology');
    }

    this.topology = topology;
    this.sessions = [];
  }

  endAllPooledSessions() {
    if (this.sessions.length) {
      this.topology.endSessions(this.sessions.map(session => session.id));
      this.sessions = [];
    }
  }

  /**
   * @returns {ServerSession}
   */
  acquire() {
    const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
    while (this.sessions.length) {
      const session = this.sessions.shift();
      if (!session.hasTimedOut(sessionTimeoutMinutes)) {
        return session;
      }
    }

    return new ServerSession();
  }

  /**
   *
   * @param {*} session
   */
  release(session) {
    const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
    while (this.sessions.length) {
      const session = this.sessions[this.sessions.length - 1];
      if (session.hasTimedOut(sessionTimeoutMinutes)) {
        this.sessions.pop();
      } else {
        break;
      }
    }

    if (!session.hasTimedOut(sessionTimeoutMinutes)) {
      this.sessions.unshift(session);
    }
  }
}

module.exports = {
  ClientSession: ClientSession,
  ServerSession: ServerSession,
  ServerSessionPool: ServerSessionPool
};
<commit_msg>refactor(txns): add state machine for transactions<commit_after>'use strict';

const retrieveBSON = require('./connection/utils').retrieveBSON;
const EventEmitter = require('events');
const BSON = retrieveBSON();
const Binary = BSON.Binary;
const uuidV4 = require('./utils').uuidV4;
const MongoError = require('./error').MongoError;
const isRetryableError = require('././error').isRetryableError;

function assertAlive(session, callback) {
  if (session.serverSession == null) {
    const error = new MongoError('Cannot use a session that has ended');
    if (typeof callback === 'function') {
      callback(error, null);
      return false;
    }

    throw error;
  }

  return true;
}

// Client session states
const NO_TRANSACTION = 'NO_TRANSACTION';
const STARTING_TRANSACTION = 'STARTING_TRANSACTION';
const TRANSACTION_IN_PROGRESS = 'TRANSACTION_IN_PROGRESS';
const TRANSACTION_COMMITTED = 'TRANSACTION_COMMITTED';
const TRANSACTION_ABORTED = 'TRANSACTION_ABORTED';

function txnStateTransition(txnState, nextState) {
  const validTransitions = {
    [NO_TRANSACTION]: [NO_TRANSACTION, STARTING_TRANSACTION],
    [STARTING_TRANSACTION]: [TRANSACTION_IN_PROGRESS, TRANSACTION_COMMITTED, TRANSACTION_ABORTED],
    [TRANSACTION_IN_PROGRESS]: [
      TRANSACTION_IN_PROGRESS,
      TRANSACTION_COMMITTED,
      TRANSACTION_ABORTED
    ],
    [TRANSACTION_COMMITTED]: [TRANSACTION_COMMITTED, STARTING_TRANSACTION, NO_TRANSACTION],
    [TRANSACTION_ABORTED]: [STARTING_TRANSACTION, NO_TRANSACTION]
  };

  // Get current state
  const nextStates = validTransitions[txnState.state];
  if (nextStates && nextStates.indexOf(nextState) !== -1) {
    txnState.state = nextState;
    return;
  }

  throw new MongoError(
    `ClientSession attempted illegal state transition from [${txnState.state}] to [${nextState}]`
  );
}

/** A class representing a client session on the server */
class ClientSession extends EventEmitter {
  /**
   * Create a client session.
   * WARNING: not meant to be instantiated directly
   *
   * @param {Topology} topology The current client's topology
   * @param {ServerSessionPool} sessionPool The server session pool
   * @param {Object} [options] Optional settings
   * @param {Boolean} [options.causalConsistency] Whether causal consistency should be enabled on this session
   * @param {Boolean} [options.autoStartTransaction=false] When enabled this session automatically starts a transaction with the provided defaultTransactionOptions.
   * @param {Object} [options.defaultTransactionOptions] The default TransactionOptions to use for transactions started on this session.
   * @param {Object} [clientOptions] Optional settings provided when creating a client in the porcelain driver
   */
  constructor(topology, sessionPool, options, clientOptions) {
    super();

    if (topology == null) {
      throw new Error('ClientSession requires a topology');
    }

    if (sessionPool == null || !(sessionPool instanceof ServerSessionPool)) {
      throw new Error('ClientSession requires a ServerSessionPool');
    }

    options = options || {};
    this.topology = topology;
    this.sessionPool = sessionPool;
    this.hasEnded = false;
    this.serverSession = sessionPool.acquire();
    this.clientOptions = clientOptions;

    this.supports = {
      causalConsistency:
        typeof options.causalConsistency !== 'undefined' ? options.causalConsistency : true
    };

    options = options || {};
    if (typeof options.initialClusterTime !== 'undefined') {
      this.clusterTime = options.initialClusterTime;
    } else {
      this.clusterTime = null;
    }

    this.operationTime = null;
    this.explicit = !!options.explicit;
    this.owner = options.owner;
    this.transactionOptions = null;
    this.autoStartTransaction = options.autoStartTransaction;
    this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
  }

  /**
   * Ends this session on the server
   *
   * @param {Object} [options] Optional settings
   * @param {Function} [callback] Optional callback for completion of this operation
   */
  endSession(options, callback) {
    if (typeof options === 'function') (callback = options), (options = {});
    options = options || {};

    if (this.hasEnded) {
      if (typeof callback === 'function') callback(null, null);
      return;
    }

    if (this.serverSession && this.inTransaction()) {
      this.abortTransaction(); // pass in callback?
    }

    // mark the session as ended, and emit a signal
    this.hasEnded = true;
    this.emit('ended', this);

    // release the server session back to the pool
    this.sessionPool.release(this.serverSession);

    // spec indicates that we should ignore all errors for `endSessions`
    if (typeof callback === 'function') callback(null, null);
  }

  /**
   * Advances the operationTime for a ClientSession.
   *
   * @param {object} operationTime the `BSON.Timestamp` of the operation type it is desired to advance to
   */
  advanceOperationTime(operationTime) {
    if (this.operationTime == null) {
      this.operationTime = operationTime;
      return;
    }

    if (operationTime.greaterThan(this.operationTime)) {
      this.operationTime = operationTime;
    }
  }

  /**
   * Used to determine if this session equals another
   */
  equals(session) {
    if (!(session instanceof ClientSession)) {
      return false;
    }

    return this.id.id.buffer.equals(session.id.id.buffer);
  }

  /**
   * Increment the transaction number on the internal ServerSession
   */
  incrementTransactionNumber() {
    this.serverSession.txnNumber++;
  }

  /**
   * @returns whether this session is current in a transaction or not
   */
  inTransaction() {
    return this.transactionOptions != null;
  }

  /**
   * Starts a new transaction with the given options.
   *
   * @param {Object} options Optional settings
   * @param {ReadConcern} [options.readConcern] The readConcern to use for this transaction
   * @param {WriteConcern} [options.writeConcern] The writeConcern to use for this transaction
   */
  startTransaction(options) {
    assertAlive(this);
    if (this.inTransaction()) {
      throw new MongoError('Transaction already in progress');
    }

    // increment txnNumber and reset stmtId to zero.
    this.serverSession.txnNumber += 1;

    // set transaction options, we will use this to determine if we are in a transaction
    this.transactionOptions = Object.assign({}, options || this.defaultTransactionOptions);
  }

  /**
   * Commits the currently active transaction in this session.
   *
   * @param {Function} [callback] optional callback for completion of this operation
   * @return {Promise} A promise is returned if no callback is provided
   */
  commitTransaction(callback) {
    if (typeof callback === 'function') {
      endTransaction(this, 'commitTransaction', callback);
      return;
    }

    return new Promise((resolve, reject) => {
      endTransaction(
        this,
        'commitTransaction',
        (err, reply) => (err ? reject(err) : resolve(reply))
      );
    });
  }

  /**
   * Aborts the currently active transaction in this session.
   *
   * @param {Function} [callback] optional callback for completion of this operation
   * @return {Promise} A promise is returned if no callback is provided
   */
  abortTransaction(callback) {
    if (typeof callback === 'function') {
      endTransaction(this, 'abortTransaction', callback);
      return;
    }

    return new Promise((resolve, reject) => {
      endTransaction(
        this,
        'abortTransaction',
        (err, reply) => (err ? reject(err) : resolve(reply))
      );
    });
  }
}

function resetTransactionState(clientSession) {
  clientSession.transactionOptions = null;
}

function endTransaction(clientSession, commandName, callback) {
  if (!assertAlive(clientSession, callback)) {
    // checking result in case callback was called
    return;
  }

  if (!clientSession.inTransaction()) {
    if (clientSession.autoStartTransaction) {
      clientSession.startTransaction();
    } else {
      callback(new MongoError('No transaction started'));
      return;
    }
  }

  if (clientSession.serverSession.stmtId === 0) {
    // The server transaction was never started.
    resetTransactionState(clientSession);
    callback(null, null);
    return;
  }

  const command = { [commandName]: 1 };
  if (clientSession.transactionOptions.writeConcern) {
    Object.assign(command, { writeConcern: clientSession.transactionOptions.writeConcern });
  } else if (clientSession.clientOptions && clientSession.clientOptions.w) {
    Object.assign(command, { writeConcern: { w: clientSession.clientOptions.w } });
  }

  function commandHandler(e, r) {
    resetTransactionState(clientSession);
    callback(e, r);
  }

  function transactionError(err) {
    return commandName === 'commitTransaction' ? err : null;
  }

  // send the command
  clientSession.topology.command(
    'admin.$cmd',
    command,
    { session: clientSession },
    (err, reply) => {
      if (err && isRetryableError(err)) {
        return clientSession.topology.command(
          'admin.$cmd',
          command,
          { session: clientSession },
          (_err, _reply) => commandHandler(transactionError(_err), _reply)
        );
      }

      commandHandler(transactionError(err), reply);
    }
  );
}

Object.defineProperty(ClientSession.prototype, 'id', {
  get: function() {
    return this.serverSession.id;
  }
});

/**
 *
 */
class ServerSession {
  constructor() {
    this.id = { id: new Binary(uuidV4(), Binary.SUBTYPE_UUID) };
    this.lastUse = Date.now();
    this.txnNumber = 0;
  }

  /**
   *
   * @param {*} sessionTimeoutMinutes
   */
  hasTimedOut(sessionTimeoutMinutes) {
    // Take the difference of the lastUse timestamp and now, which will result in a value in
    // milliseconds, and then convert milliseconds to minutes to compare to `sessionTimeoutMinutes`
    const idleTimeMinutes = Math.round(
      (((Date.now() - this.lastUse) % 86400000) % 3600000) / 60000
    );

    return idleTimeMinutes > sessionTimeoutMinutes - 1;
  }
}

/**
 *
 */
class ServerSessionPool {
  constructor(topology) {
    if (topology == null) {
      throw new Error('ServerSessionPool requires a topology');
    }

    this.topology = topology;
    this.sessions = [];
  }

  endAllPooledSessions() {
    if (this.sessions.length) {
      this.topology.endSessions(this.sessions.map(session => session.id));
      this.sessions = [];
    }
  }

  /**
   * @returns {ServerSession}
   */
  acquire() {
    const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
    while (this.sessions.length) {
      const session = this.sessions.shift();
      if (!session.hasTimedOut(sessionTimeoutMinutes)) {
        return session;
      }
    }

    return new ServerSession();
  }

  /**
   *
   * @param {*} session
   */
  release(session) {
    const sessionTimeoutMinutes = this.topology.logicalSessionTimeoutMinutes;
    while (this.sessions.length) {
      const session = this.sessions[this.sessions.length - 1];
      if (session.hasTimedOut(sessionTimeoutMinutes)) {
        this.sessions.pop();
      } else {
        break;
      }
    }

    if (!session.hasTimedOut(sessionTimeoutMinutes)) {
      this.sessions.unshift(session);
    }
  }
}

module.exports = {
  ClientSession: ClientSession,
  ServerSession: ServerSession,
  ServerSessionPool: ServerSessionPool
};
<|endoftext|>


<commit_before>(function(module) {
  'use strict';

  var user = module.parent.require('./user'),
    meta = module.parent.require('./meta'),
    db = module.parent.require('../src/database'),
    passport = module.parent.require('passport'),
    passportClientCert = require('passport-client-cert').Strategy,
    nconf = module.parent.require('nconf'),
    async = module.parent.require('async'),
    winston = module.parent.require('winston');

  var constants = Object.freeze({
    'name': 'client-cert',
    'icon': 'icon-client-cert-auth'
  });

  var CA_CNs = ['ESTEID-SK 2007', 'ESTEID-SK 2011'];
  // TODO: make a settings page in the Admin CP to configure these
  // and success/failure URLs.

  var ClientCert = {};

  ClientCert.getStrategy = function(strategies, callback) {
    passport.use(constants.name, new passportClientCert({
        passReqToCallback: true,
        successReturnToOrRedirect: '/',
        failureRedirect: '/client-cert-auth-error'
    }, function(req, cert, done) {
      var issuer = cert.issuer;
      var issuerCommonName = issuer.CN || issuer.commonName;

      if (CA_CNs.indexOf(issuerCommonName) === -1) {
        winston.error('[sso-client-cert] Client Certificate Issuer CN invalid: ' + issuerCommonName);
        return done(null, false);
      }

      var subject = cert.subject;
      var subjectCommonName = subject.CN || subject.commonName;

      if (!subject) {
        winston.error('[sso-client-cert] Client Certificate Subject missing!');
        return done(null, false);
      } else if (!subjectCommonName) {
        winston.error('[sso-client-cert] Client Certificate CN missing!');
        return done(null, false);
      }

      if (req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && req.user.uid > 0) {
        // Save cert CN to the user
        user.setUserField(req.user.uid, 'certcn', subjectCommonName);
        db.setObjectField('certcn:uid', subjectCommonName, req.user.uid);
        return done(null, req.user);
      }

      var subjectGivenName = subject.GN || subject.givenName;
      var subjectSurname = subject.SN || subject.surname;
      var firstName = subjectGivenName.charAt(0).toUpperCase() + subjectGivenName.slice(1).toLowerCase();
      var lastName = subjectSurname.charAt(0).toUpperCase() + subjectSurname.slice(1).toLowerCase();
      var userName = firstName + ' ' + lastName;
      var email = "";

      if (cert.subjectaltname) {
        email = cert.subjectaltname.split('email:')[1];
      }

      ClientCert.login(subjectCommonName, userName, email, function(err, user) {
        if (err) {
          return done(err);
        }

        done(null, user);
      });
    }));

    strategies.push({
      name: 'client-cert',
      url: '/auth/client-cert',
      callbackURL: '/',
      icon: constants.icon
    });

    callback(null, strategies);
  };

  ClientCert.getAssociation = function(data, callback) {
    user.getUserField(data.uid, 'certcn', function(err, CN) {
      if (err) {
        return callback(err, data);
      }

      if (CN) {
        data.associations.push({
          associated: true,
          name: constants.name,
          icon: constants.icon
        });
      } else {
        data.associations.push({
          associated: false,
          url: nconf.get('url') + '/auth/client-cert',
          name: constants.name,
          icon: constants.icon
        });
      }

      callback(null, data);
    })
  };

  ClientCert.login = function(CN, userName, email, callback) {
    ClientCert.getUidByCertCN(CN, function(err, uid) {
      if(err) {
        return callback(err);
      }

      if (uid !== null) {
        // Existing User
        callback(null, {
          uid: uid
        });
      } else {
        // New User
        var success = function(uid) {
          // Save cert CN to the user
          user.setUserField(uid, 'certcn', CN);
          db.setObjectField('certcn:uid', CN, uid);
          user.setUserField(uid, 'email:confirmed', 0); // TODO: Add option to configure in Admin CP

          callback(null, {
            uid: uid
          });
        };

        user.getUidByEmail(email, function(err, uid) {
          if(err) {
            return callback(err);
          }

          if (!uid) {
            user.create({username: userName, email: email}, function(err, uid) {
              if(err) {
                return callback(err);
              }

              success(uid);
            });
          } else {
            success(uid); // Existing account -- merge
          }
        });
      }
    });
  };

  ClientCert.getUidByCertCN = function(CN, callback) {
    db.getObjectField('certcn:uid', CN, function(err, uid) {
      if (err) {
        return callback(err);
      }
      callback(null, uid);
    });
  };

  ClientCert.deleteUserData = function(data, callback) {
    var uid = data.uid;

    async.waterfall([
      async.apply(user.getUserField, uid, 'certcn'),
      function(CNToDelete, next) {
        db.deleteObjectField('certcn:uid', CNToDelete, next);
      }
    ], function(err) {
      if (err) {
        winston.error('[sso-client-cert] Could not remove cert CN for uid ' + uid + '. Error: ' + err);
        return callback(err);
      }
      callback(null, uid);
    });
  };

  module.exports = ClientCert;
}(module));<commit_msg>Update for Apache<commit_after>(function(module) {
  'use strict';

  var user = module.parent.require('./user'),
    meta = module.parent.require('./meta'),
    db = module.parent.require('../src/database'),
    passport = module.parent.require('passport'),
    passportClientCert = require('passport-client-cert').Strategy,
    nconf = module.parent.require('nconf'),
    async = module.parent.require('async'),
    winston = module.parent.require('winston');

  var constants = Object.freeze({
    'name': 'client-cert',
    'icon': 'icon-client-cert-auth'
  });

  var CA_CNs = ['ESTEID-SK 2007', 'ESTEID-SK 2011'];
  // TODO: make a settings page in the Admin CP to configure these
  // and success/failure URLs.

  var ClientCert = {};

  ClientCert.getStrategy = function(strategies, callback) {
    passport.use(constants.name, new passportClientCert({
        passReqToCallback: true,
        successReturnToOrRedirect: '/',
        failureRedirect: '/client-cert-auth-error'
    }, function(req, cert, done) {
      var issuer = cert.issuer ? cert.issuer : {CN: undefined};
      var issuerCommonName = issuer.CN ? issuer.CN : req.headers.ssl_client_i_dn_cn;

      if (CA_CNs.indexOf(issuerCommonName) === -1) {
        winston.error('[sso-client-cert] Client Certificate Issuer CN invalid: ' + issuerCommonName);
        return done(null, false);
      }

      var subject = cert.subject ? cert.subject : {CN: undefined, GN: undefined, SN: undefined};
      var subjectCommonName = subject.CN ? subject.CN : req.headers.ssl_client_s_dn_cn;

      if (!subject) {
        winston.error('[sso-client-cert] Client Certificate Subject missing!');
        return done(null, false);
      } else if (!subjectCommonName) {
        winston.error('[sso-client-cert] Client Certificate Subject CN missing!');
        return done(null, false);
      }

      if (req.hasOwnProperty('user') && req.user.hasOwnProperty('uid') && req.user.uid > 0) {
        // Save cert CN to the user
        user.setUserField(req.user.uid, 'certcn', subjectCommonName);
        db.setObjectField('certcn:uid', subjectCommonName, req.user.uid);
        return done(null, req.user);
      }

      var subjectGivenName = subject.GN ? subject.GN : req.headers.ssl_client_s_dn_g;
      var subjectSurname = subject.SN ? subject.SN : req.headers.ssl_client_s_dn_s;
      var firstName = subjectGivenName.charAt(0).toUpperCase() + subjectGivenName.slice(1).toLowerCase();
      var lastName = subjectSurname.charAt(0).toUpperCase() + subjectSurname.slice(1).toLowerCase();
      var userName = firstName + ' ' + lastName;
      var email = "";

      if (cert.subjectaltname) {
        email = cert.subjectaltname.split('email:')[1];
      }

      ClientCert.login(subjectCommonName, userName, email, function(err, user) {
        if (err) {
          return done(err);
        }

        done(null, user);
      });
    }));

    strategies.push({
      name: 'client-cert',
      url: '/auth/client-cert',
      callbackURL: '/',
      icon: constants.icon
    });

    callback(null, strategies);
  };

  ClientCert.getAssociation = function(data, callback) {
    user.getUserField(data.uid, 'certcn', function(err, CN) {
      if (err) {
        return callback(err, data);
      }

      if (CN) {
        data.associations.push({
          associated: true,
          name: constants.name,
          icon: constants.icon
        });
      } else {
        data.associations.push({
          associated: false,
          url: nconf.get('url') + '/auth/client-cert',
          name: constants.name,
          icon: constants.icon
        });
      }

      callback(null, data);
    })
  };

  ClientCert.login = function(CN, userName, email, callback) {
    ClientCert.getUidByCertCN(CN, function(err, uid) {
      if(err) {
        return callback(err);
      }

      if (uid !== null) {
        // Existing User
        callback(null, {
          uid: uid
        });
      } else {
        // New User
        var success = function(uid) {
          // Save cert CN to the user
          user.setUserField(uid, 'certcn', CN);
          db.setObjectField('certcn:uid', CN, uid);
          user.setUserField(uid, 'email:confirmed', 0); // TODO: Add option to configure in Admin CP

          callback(null, {
            uid: uid
          });
        };

        user.getUidByEmail(email, function(err, uid) {
          if(err) {
            return callback(err);
          }

          if (!uid) {
            user.create({username: userName, email: email}, function(err, uid) {
              if(err) {
                return callback(err);
              }

              success(uid);
            });
          } else {
            success(uid); // Existing account -- merge
          }
        });
      }
    });
  };

  ClientCert.getUidByCertCN = function(CN, callback) {
    db.getObjectField('certcn:uid', CN, function(err, uid) {
      if (err) {
        return callback(err);
      }
      callback(null, uid);
    });
  };

  ClientCert.deleteUserData = function(data, callback) {
    var uid = data.uid;

    async.waterfall([
      async.apply(user.getUserField, uid, 'certcn'),
      function(CNToDelete, next) {
        db.deleteObjectField('certcn:uid', CNToDelete, next);
      }
    ], function(err) {
      if (err) {
        winston.error('[sso-client-cert] Could not remove cert CN for uid ' + uid + '. Error: ' + err);
        return callback(err);
      }
      callback(null, uid);
    });
  };

  module.exports = ClientCert;
}(module));<|endoftext|>


<commit_before>"use strict";

var smallModifiers = ['kon', 'lili', 'mute', 'sin'],
    narrowModifiers = ['wan', 'tu', 'anu', 'en', 'kin'];

function getSizeOf(token) {
    if (smallModifiers.indexOf(token) > -1) {
        return [1, 0.5];
    } else if (narrowModifiers.indexOf(token) > -1) {
        return [0.5, 1];
    } else {
        return [1, 1];
    }
}

var options = [];

function go(tokens, state, size, direction, index, length) {
    var newState = state.slice(0),
        newSize = size.slice(0);

    var prevSize = getSizeOf(tokens[index]),
        glyphPosition = [direction === 'right' ? size[0] : 0, direction === 'down' ? size[1] : 0];

    for (var i = index; i < index + length; i++) {
        var addSize = getSizeOf(tokens[i]),
            glyphSize = [];
        switch (direction) {
            case 'right':
                // check if all have the same height
                if (addSize[1] !== prevSize[1]) {
                    // if not skip this option
                    return;
                }
                glyphSize = [addSize[0] / addSize[1] * size[1], newSize[1]];
                newSize[0] += glyphSize[0];
                break;
            case 'down':
                // check if all have the same width
                if (addSize[0] !== prevSize[0]) {
                    // if not skip this option
                    return;
                }
                glyphSize = [newSize[0], addSize[1] / addSize[0] * size[0]];
                newSize[1] += addSize[1] / addSize[0] * size[0];
                break;
        }
        prevSize = addSize;
        newState.push({token: tokens[i], direction: direction, size: glyphSize, position: glyphPosition});

        // update glyphposition
        glyphPosition = [glyphPosition[0] + (direction === 'right' ? glyphSize[0] : 0),
            glyphPosition[1] + (direction === 'down' ? glyphSize[1] : 0)];
    }

    if (index + length === tokens.length) {
        options.push({
            state: newState,
            size: newSize,
            ratio: newSize[0] / newSize[1],
            normedRatio: newSize[0] / newSize[1] < 1 ? newSize[0] / newSize[1] : newSize[1] / newSize[0],
            surface: newSize[0] * newSize[1]
        });
        return;
    }

    for (var j = 1; j < tokens.length - index; j++) {
        go(tokens, newState, newSize, 'right', index + length, j);
        go(tokens, newState, newSize, 'down', index + length, j);
    }
}

function convertNounPhrase(tokens) {
    // choose both to go right and down
    go(tokens, [{token: tokens[0], size: [1, 1], position: [0, 0]}], [1, 1], 'right', 1, 1);
    go(tokens, [{token: tokens[0], size: [1, 1], position: [0, 0]}], [1, 1], 'down', 1, 1);

    options.sort(function (a, b) {
        return a.surface - b.surface;
        //return b.ratio - a.ratio;
    });

    console.log(options);
    options.forEach(function (option) {
        renderOption(option);
    });

}

function renderOption(option) {
    var container = document.createElement('div');
    container.classList.add('toki-nounphrase');
    container.style.width = option.size[0] + 'em';
    container.style.height = option.size[1] + 'em';

    option.state.forEach(function (glyph) {
        var element = document.createElement('div');
        element.style.width = glyph.size[0] + 'em';
        element.style.height = glyph.size[1] + 'em';
        element.style.left = glyph.position[0] + 'em';
        element.style.top = glyph.position[1] + 'em';
        element.setAttribute('data-toki-word', glyph.token);

        container.appendChild(element);
    });
    document.getElementById('sitelen').appendChild(container);
}

var tokens = ['jan', 'utala', 'pona', 'wan', 'lili', 'wan'];

setTimeout(function () {
    convertNounPhrase(tokens);
}, 500);

//function convertToInstructions(structuredSentence) {
//
//    var instructions = [],
//        size;
//
//    structuredSentence.forEach(function (part) {
//
//        switch (part.part) {
//            case 'subject':
//            case 'verbPhrase':
//            case 'directObject':
//                instructions.push.apply(instructions, convertNounPhrase(part));
//                break;
//            case 'punctuation':
//                instructions.push({rule: 'addPunctuation', glyph: part.token === '.' ? 'period' : '', size: 'wide'});
//                break;
//            default:
//                console.log('ERR: unknown part');
//                break;
//        }
//    });
//
//    // determine container sizes
//    for (var i = 0; i < instructions.length; i++) {
//        if (instructions[i].rule === 'openContainer') {
//            var closure = 0, regularGlyphs = 0, narrowGlyphs = 0;
//            for (var j = i; j < instructions.length; j++) {
//                switch (instructions[j].rule) {
//                    case 'openContainer':
//                        closure++;
//                        break;
//                    case 'closeContainer':
//                        closure--;
//                        if (closure === 0) {
//                            // round narrow glyphs
//                            var roundglyphs = Math.round(regularGlyphs + narrowGlyphs / 2);
//                            // add metadata to the container
//                            instructions[i].size = roundglyphs === 1 ? 'regular' :
//                                roundglyphs > 3 || (roundglyphs === 3 && narrowGlyphs === 1) ? 'double' : 'wide';
//                            instructions[i].children = roundglyphs;
//                            closure = -1;
//                        }
//                        break;
//                    case 'addGlyph':
//                        // narrow glyphs only count as half
//                        if (narrowModifiers.indexOf(instructions[j].glyph) > -1) {
//                            narrowGlyphs++;
//                        } else {
//                            regularGlyphs++;
//                        }
//                        break;
//                }
//                if (closure === -1) {
//                    break;
//                }
//            }
//        }
//    }
//    return instructions;
//}

<commit_msg>Improved layout algorithm.<commit_after>"use strict";

var smallModifiers = ['kon', 'lili', 'mute', 'sin'],
    narrowModifiers = ['wan', 'tu', 'anu', 'en', 'kin'];

function getSizeOf(token) {
    if (smallModifiers.indexOf(token) > -1) {
        return [1, 0.5];
    } else if (narrowModifiers.indexOf(token) > -1) {
        return [0.5, 1];
    } else {
        return [1, 1];
    }
}

var options = [], hash = {};

function go(tokens, state, size, direction, index, length) {

    var newState = state.slice(0),
        newSize = size.slice(0);

    var prevSize = getSizeOf(tokens[index]),
        glyphPosition = [direction === 'right' ? size[0] : 0, direction === 'down' ? size[1] : 0];

    var sizeSum = [0, 0], addSize;
    // determine size sum
    for (var i = index; i < index + length; i++) {
        addSize = getSizeOf(tokens[i]);
        if ((direction === 'down' && addSize[1] !== prevSize[1]) ||
            (direction === 'right' && addSize[0] !== prevSize[0])) {

            console.log('returning ', state.length, index, length);
            return;
        }
        sizeSum = [sizeSum[0] + addSize[0], sizeSum[1] + addSize[1]];
    }

    var actualAdded = 0;
    // now add the glyphs one by one
    for (var i = index; i < index + length; i++) {
        var glyphSize = [], addX = 0, addY = 0;
        addSize = getSizeOf(tokens[i]);

        switch (direction) {
            case 'down':
                addY = addSize[1] * size[0] / sizeSum[0];
                glyphSize = [addSize[0] * addY / addSize[1], addY];

                newSize = [size[0], size[1] + addY];

                break;
            case 'right':
                addX = addSize[0] * size[1] / sizeSum[1];
                glyphSize = [addX, addSize[1] * addX / addSize[0]];

                newSize = [size[0] + addX, size[1]];
                break;
        }
        prevSize = addSize;
        newState.push({token: tokens[i], size: glyphSize, position: glyphPosition});
        actualAdded++;

        // update glyphposition
        glyphPosition = [glyphPosition[0] + (direction === 'down' ? glyphSize[0] : 0),
            glyphPosition[1] + (direction === 'right' ? glyphSize[1] : 0)];
    }

    if (index + actualAdded === tokens.length) {
        var newOption = {
            state: newState,
            size: newSize,
            ratio: newSize[0] / newSize[1],
            normedRatio: newSize[0] / newSize[1] < 1 ? newSize[0] / newSize[1] : newSize[1] / newSize[0],
            surface: newSize[0] * newSize[1]
        };
        if (!hash[JSON.stringify(newOption)]) {
            options.push(newOption);
            hash[JSON.stringify(newOption)] = newOption;
        }
        return;
    }

    for (var j = 1; j < tokens.length - index; j++) {
        go(tokens, newState, newSize, 'right', index + actualAdded, j);
        go(tokens, newState, newSize, 'down', index + actualAdded, j);
    }
}

function convertNounPhrase(tokens) {
    // choose both to go right and down
    for (var j = 1; j < tokens.length; j++) {
        go(tokens, [{token: tokens[0], size: [1, 1], position: [0, 0]}], [1, 1], 'right', 1, j);
        go(tokens, [{token: tokens[0], size: [1, 1], position: [0, 0]}], [1, 1], 'down', 1, j);
    }

    options.sort(function (a, b) {
        return a.surface - b.surface;
    });

    console.log(options);
    options.forEach(function (option) {
        renderOption(option);
    });

}

function renderOption(option) {
    var container = document.createElement('div');
    container.classList.add('toki-nounphrase');
    container.style.width = option.size[0] + 'em';
    container.style.height = option.size[1] + 'em';

    option.state.forEach(function (glyph) {
        var element = document.createElement('div');
        element.style.width = glyph.size[0] + 'em';
        element.style.height = glyph.size[1] + 'em';
        element.style.left = glyph.position[0] + 'em';
        element.style.top = glyph.position[1] + 'em';
        element.setAttribute('data-toki-word', glyph.token);

        container.appendChild(element);
    });
    document.getElementById('sitelen').appendChild(container);
}

var tokens = ['jan', 'utala', 'pona', 'wan', 'lili', 'wan'];
//var tokens = ['jan', 'utala', 'pona', 'mi', 'wan'];
//var tokens = ['jan', 'wan'];

setTimeout(function () {
    convertNounPhrase(tokens);
}, 500);

//function convertToInstructions(structuredSentence) {
//
//    var instructions = [],
//        size;
//
//    structuredSentence.forEach(function (part) {
//
//        switch (part.part) {
//            case 'subject':
//            case 'verbPhrase':
//            case 'directObject':
//                instructions.push.apply(instructions, convertNounPhrase(part));
//                break;
//            case 'punctuation':
//                instructions.push({rule: 'addPunctuation', glyph: part.token === '.' ? 'period' : '', size: 'wide'});
//                break;
//            default:
//                console.log('ERR: unknown part');
//                break;
//        }
//    });
//
//    // determine container sizes
//    for (var i = 0; i < instructions.length; i++) {
//        if (instructions[i].rule === 'openContainer') {
//            var closure = 0, regularGlyphs = 0, narrowGlyphs = 0;
//            for (var j = i; j < instructions.length; j++) {
//                switch (instructions[j].rule) {
//                    case 'openContainer':
//                        closure++;
//                        break;
//                    case 'closeContainer':
//                        closure--;
//                        if (closure === 0) {
//                            // round narrow glyphs
//                            var roundglyphs = Math.round(regularGlyphs + narrowGlyphs / 2);
//                            // add metadata to the container
//                            instructions[i].size = roundglyphs === 1 ? 'regular' :
//                                roundglyphs > 3 || (roundglyphs === 3 && narrowGlyphs === 1) ? 'double' : 'wide';
//                            instructions[i].children = roundglyphs;
//                            closure = -1;
//                        }
//                        break;
//                    case 'addGlyph':
//                        // narrow glyphs only count as half
//                        if (narrowModifiers.indexOf(instructions[j].glyph) > -1) {
//                            narrowGlyphs++;
//                        } else {
//                            regularGlyphs++;
//                        }
//                        break;
//                }
//                if (closure === -1) {
//                    break;
//                }
//            }
//        }
//    }
//    return instructions;
//}

<|endoftext|>


<commit_before>const db = require('../lib/Database').db
const sql = require('sqlate')
const log = require('../lib/Log')('Media')
const Queue = require('../Queue')

class Media {
  /**
   * Get media matching all search criteria
   *
   * @param  {Object}  filter Search criteria
   * @return {Promise}        Object with media results
   */
  static async search (filter) {
    const media = {
      result: [],
      entities: {}
    }

    const whereClause = typeof filter !== 'object'
      ? sql`true`
      : sql`${sql.tuple(Object.keys(filter).map(sql.column))} = ${sql.tuple(Object.values(filter))}`

    const query = sql`
      SELECT
        media.*,
        songs.*,
        artists.artistId, artists.name AS artist, artists.nameNorm AS artistNorm,
        paths.pathId, paths.path
      FROM media
        INNER JOIN songs USING (songId)
        INNER JOIN artists USING (artistId)
        INNER JOIN paths USING (pathId)
      WHERE ${whereClause}
      ORDER BY paths.priority ASC
    `
    const rows = await db.all(String(query), query.parameters)

    for (const row of rows) {
      media.result.push(row.mediaId)
      media.entities[row.mediaId] = row
    }

    return media
  }

  /**
   * Add media file to the library
   *
   * @param  {Object}  media Media object
   * @return {Number}        New media's mediaId
   */
  static async add (media) {
    if (!Number.isInteger(media.songId) ||
        !Number.isInteger(media.duration) ||
        !Number.isInteger(media.pathId) ||
        !media.relPath
    ) throw new Error('invalid media data: ' + JSON.stringify(media))

    // currently uses an Object instead of Map
    const query = sql`
      INSERT INTO media ${sql.tuple(Object.keys(media).map(sql.column))}
      VALUES ${sql.tuple(Object.values(media))}
    `
    const res = await db.run(String(query), query.parameters)

    if (!Number.isInteger(res.lastID)) {
      throw new Error('invalid lastID from media insert')
    }

    return res.lastID
  }

  /**
   * Update media item
   *
   * @param  {Object}  media Media object
   * @return {Promise}
   */
  static async update (media) {
    const { mediaId } = media

    if (!Number.isInteger(mediaId)) {
      throw new Error(`invalid mediaId: ${mediaId}`)
    }

    // currently uses an Object instead of Map
    delete media.mediaId

    const query = sql`
      UPDATE media
      SET ${sql.tuple(Object.keys(media).map(sql.column))} = ${sql.tuple(Object.values(media))}
      WHERE mediaId = ${mediaId}
    `
    await db.run(String(query), query.parameters)
  }

  /**
   * Removes media from the db in sqlite-friendly batches
   *
   * @param  {Array}  mediaIds
   * @return {Promise}
   */
  static async remove (mediaIds) {
    const batchSize = 999

    while (mediaIds.length) {
      const query = sql`
        DELETE FROM media
        WHERE mediaId IN ${sql.in(mediaIds.splice(0, batchSize))}
      `
      const res = await db.run(String(query), query.parameters)

      log.info(`removed ${res.changes} media`)
    }
  }

  /**
   * Remove unlinked items and VACUUM
   *
   * @return {Promise}
   */
  static async cleanup () {
    let res

    // remove media in nonexistent paths
    res = await db.run(`
      DELETE FROM media WHERE mediaId IN (
        SELECT media.mediaId FROM media LEFT JOIN paths USING(pathId) WHERE paths.pathId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} media in nonexistent paths`)

    // remove songs without associated media
    res = await db.run(`
      DELETE FROM songs WHERE songId IN (
        SELECT songs.songId FROM songs LEFT JOIN media USING(songId) WHERE media.mediaId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} songs with no associated media`)

    // remove stars for nonexistent songs
    res = await db.run(`
      DELETE FROM songStars WHERE songId IN (
        SELECT songStars.songId FROM songStars LEFT JOIN songs USING(songId) WHERE songs.songId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} stars for nonexistent songs`)

    // remove queue items for nonexistent songs
    const rows = await db.all(`
      SELECT queue.queueId FROM queue LEFT JOIN songs USING(songId) WHERE songs.songId IS NULL
    `)

    for (const row of rows) {
      await Queue.remove({ queueId: row.queueId })
    }

    log.info(`cleanup: ${rows.length} queue items for nonexistent songs`)

    log.info('cleanup: vacuuming database')
    await db.run('VACUUM')
  }

  /**
   * Set isPreferred flag for a given media item
   * @param  {Number}  mediaId
   * @param  {Boolean} isPreferred
   * @return {Promise} songId of the (un)preferred media item
   */
  static async setPreferred (mediaId, isPreferred) {
    if (!Number.isInteger(mediaId) || typeof isPreferred !== 'boolean') {
      throw new Error('invalid mediaId or value')
    }

    // get songId
    const res = await Media.search({ mediaId })

    if (!res.result.length) {
      throw new Error(`mediaId not found: ${mediaId}`)
    }

    const songId = res.entities[mediaId].songId

    // clear any currently preferred items
    const query = sql`
      UPDATE media
      SET isPreferred = 0
      WHERE songId = ${songId}
    `
    await db.run(String(query), query.parameters)

    if (isPreferred) {
      await Media.update({ mediaId, isPreferred: 1 })
    }

    return songId
  }
}

Media.mimeTypes = {
  mp3: 'audio/mpeg',
  m4a: 'audio/mp4',
  mp4: 'video/mp4',
  cdg: 'application/octet-stream',
}

module.exports = Media
<commit_msg>fix: usage of Queue.remove() in cleanup<commit_after>const db = require('../lib/Database').db
const sql = require('sqlate')
const log = require('../lib/Log')('Media')
const Queue = require('../Queue')

class Media {
  /**
   * Get media matching all search criteria
   *
   * @param  {Object}  filter Search criteria
   * @return {Promise}        Object with media results
   */
  static async search (filter) {
    const media = {
      result: [],
      entities: {}
    }

    const whereClause = typeof filter !== 'object'
      ? sql`true`
      : sql`${sql.tuple(Object.keys(filter).map(sql.column))} = ${sql.tuple(Object.values(filter))}`

    const query = sql`
      SELECT
        media.*,
        songs.*,
        artists.artistId, artists.name AS artist, artists.nameNorm AS artistNorm,
        paths.pathId, paths.path
      FROM media
        INNER JOIN songs USING (songId)
        INNER JOIN artists USING (artistId)
        INNER JOIN paths USING (pathId)
      WHERE ${whereClause}
      ORDER BY paths.priority ASC
    `
    const rows = await db.all(String(query), query.parameters)

    for (const row of rows) {
      media.result.push(row.mediaId)
      media.entities[row.mediaId] = row
    }

    return media
  }

  /**
   * Add media file to the library
   *
   * @param  {Object}  media Media object
   * @return {Number}        New media's mediaId
   */
  static async add (media) {
    if (!Number.isInteger(media.songId) ||
        !Number.isInteger(media.duration) ||
        !Number.isInteger(media.pathId) ||
        !media.relPath
    ) throw new Error('invalid media data: ' + JSON.stringify(media))

    // currently uses an Object instead of Map
    const query = sql`
      INSERT INTO media ${sql.tuple(Object.keys(media).map(sql.column))}
      VALUES ${sql.tuple(Object.values(media))}
    `
    const res = await db.run(String(query), query.parameters)

    if (!Number.isInteger(res.lastID)) {
      throw new Error('invalid lastID from media insert')
    }

    return res.lastID
  }

  /**
   * Update media item
   *
   * @param  {Object}  media Media object
   * @return {Promise}
   */
  static async update (media) {
    const { mediaId } = media

    if (!Number.isInteger(mediaId)) {
      throw new Error(`invalid mediaId: ${mediaId}`)
    }

    // currently uses an Object instead of Map
    delete media.mediaId

    const query = sql`
      UPDATE media
      SET ${sql.tuple(Object.keys(media).map(sql.column))} = ${sql.tuple(Object.values(media))}
      WHERE mediaId = ${mediaId}
    `
    await db.run(String(query), query.parameters)
  }

  /**
   * Removes media from the db in sqlite-friendly batches
   *
   * @param  {Array}  mediaIds
   * @return {Promise}
   */
  static async remove (mediaIds) {
    const batchSize = 999

    while (mediaIds.length) {
      const query = sql`
        DELETE FROM media
        WHERE mediaId IN ${sql.in(mediaIds.splice(0, batchSize))}
      `
      const res = await db.run(String(query), query.parameters)

      log.info(`removed ${res.changes} media`)
    }
  }

  /**
   * Remove unlinked items and VACUUM
   *
   * @return {Promise}
   */
  static async cleanup () {
    let res

    // remove media in nonexistent paths
    res = await db.run(`
      DELETE FROM media WHERE mediaId IN (
        SELECT media.mediaId FROM media LEFT JOIN paths USING(pathId) WHERE paths.pathId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} media in nonexistent paths`)

    // remove songs without associated media
    res = await db.run(`
      DELETE FROM songs WHERE songId IN (
        SELECT songs.songId FROM songs LEFT JOIN media USING(songId) WHERE media.mediaId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} songs with no associated media`)

    // remove stars for nonexistent songs
    res = await db.run(`
      DELETE FROM songStars WHERE songId IN (
        SELECT songStars.songId FROM songStars LEFT JOIN songs USING(songId) WHERE songs.songId IS NULL
      )
    `)
    log.info(`cleanup: ${res.changes} stars for nonexistent songs`)

    // remove queue items for nonexistent songs
    const rows = await db.all(`
      SELECT queue.queueId FROM queue LEFT JOIN songs USING(songId) WHERE songs.songId IS NULL
    `)

    for (const row of rows) {
      await Queue.remove(row.queueId)
    }

    log.info(`cleanup: ${rows.length} queue items for nonexistent songs`)

    log.info('cleanup: vacuuming database')
    await db.run('VACUUM')
  }

  /**
   * Set isPreferred flag for a given media item
   * @param  {Number}  mediaId
   * @param  {Boolean} isPreferred
   * @return {Promise} songId of the (un)preferred media item
   */
  static async setPreferred (mediaId, isPreferred) {
    if (!Number.isInteger(mediaId) || typeof isPreferred !== 'boolean') {
      throw new Error('invalid mediaId or value')
    }

    // get songId
    const res = await Media.search({ mediaId })

    if (!res.result.length) {
      throw new Error(`mediaId not found: ${mediaId}`)
    }

    const songId = res.entities[mediaId].songId

    // clear any currently preferred items
    const query = sql`
      UPDATE media
      SET isPreferred = 0
      WHERE songId = ${songId}
    `
    await db.run(String(query), query.parameters)

    if (isPreferred) {
      await Media.update({ mediaId, isPreferred: 1 })
    }

    return songId
  }
}

Media.mimeTypes = {
  mp3: 'audio/mpeg',
  m4a: 'audio/mp4',
  mp4: 'video/mp4',
  cdg: 'application/octet-stream',
}

module.exports = Media
<|endoftext|>


<commit_before>import React from 'react';
import SignupPage from './SignupPage';
import {render, fireEvent} from 'test-utils';

const setup = (overrides) => {
    const {mockOnActionFn, ...utils} = render(
        <SignupPage />
    );
    const emailInput = utils.getByLabelText(/email/i);
    const nameInput = utils.getByLabelText(/name/i);
    const submitButton = utils.queryByRole('button', {name: 'Continue'});
    const signinButton = utils.queryByRole('button', {name: 'Log in'});
    return {
        nameInput,
        emailInput,
        submitButton,
        signinButton,
        mockOnActionFn,
        ...utils
    };
};

describe('SignupPage', () => {
    test('renders', () => {
        const {nameInput, emailInput, submitButton, signinButton} = setup();

        expect(nameInput).toBeInTheDocument();
        expect(emailInput).toBeInTheDocument();
        expect(submitButton).toBeInTheDocument();
        expect(signinButton).toBeInTheDocument();
    });

    test('can call signup action with name, email and plan', () => {
        const {nameInput, emailInput, submitButton, mockOnActionFn} = setup();
        const nameVal = 'J Smith';
        const emailVal = 'jsmith@example.com';
        const planVal = 'FREE';

        fireEvent.change(nameInput, {target: {value: nameVal}});
        fireEvent.change(emailInput, {target: {value: emailVal}});
        expect(nameInput).toHaveValue(nameVal);
        expect(emailInput).toHaveValue(emailVal);

        fireEvent.click(submitButton);
        expect(mockOnActionFn).toHaveBeenCalledWith('signup', {email: emailVal, name: nameVal, plan: planVal});
    });

    test('can call swithPage for signin', () => {
        const {signinButton, mockOnActionFn} = setup();

        fireEvent.click(signinButton);
        expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {page: 'signin'});
    });
});
<commit_msg>Fixed tests<commit_after>import React from 'react';
import SignupPage from './SignupPage';
import {render, fireEvent} from 'test-utils';

const setup = (overrides) => {
    const {mockOnActionFn, ...utils} = render(
        <SignupPage />
    );
    const emailInput = utils.getByLabelText(/email/i);
    const nameInput = utils.getByLabelText(/name/i);
    const submitButton = utils.queryByRole('button', {name: 'Continue'});
    const signinButton = utils.queryByRole('button', {name: 'Log in'});
    return {
        nameInput,
        emailInput,
        submitButton,
        signinButton,
        mockOnActionFn,
        ...utils
    };
};

describe('SignupPage', () => {
    test('renders', () => {
        const {nameInput, emailInput, submitButton, signinButton} = setup();

        expect(nameInput).toBeInTheDocument();
        expect(emailInput).toBeInTheDocument();
        expect(submitButton).toBeInTheDocument();
        expect(signinButton).toBeInTheDocument();
    });

    test('can call signup action with name, email and plan', () => {
        const {nameInput, emailInput, submitButton, mockOnActionFn} = setup();
        const nameVal = 'J Smith';
        const emailVal = 'jsmith@example.com';
        const planVal = 'Free';

        fireEvent.change(nameInput, {target: {value: nameVal}});
        fireEvent.change(emailInput, {target: {value: emailVal}});
        expect(nameInput).toHaveValue(nameVal);
        expect(emailInput).toHaveValue(emailVal);

        fireEvent.click(submitButton);
        expect(mockOnActionFn).toHaveBeenCalledWith('signup', {email: emailVal, name: nameVal, plan: planVal});
    });

    test('can call swithPage for signin', () => {
        const {signinButton, mockOnActionFn} = setup();

        fireEvent.click(signinButton);
        expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {page: 'signin'});
    });
});
<|endoftext|>


<commit_before>var core = require('../core').core;
var moment = require('../node_modules/moment/moment');
var numeric = require('numeric');
var _ = require('underscore');

// GET Parameters:
//
//     aggregate   [Optional]  Used with: interval, start, end, geom
//     end         [Optional]  Used with: aggregate, interval, start, geom
//     geom        [Optional]  Used with: aggregate, start, end
//     interval    [Optional]  Used with: aggregate, start, end
//     start       [Optional]  Used with: aggregate, interval, end, geom
//
//     Valid combinations:
//       (geom, start, end)
//       (geom, start, end, aggregate)
//       (geom, start, end, aggregate, interval),

function roi (req, res) {
    var body, coords, grouping, i, idx, projection;
    var collection = core.DATA[req.params.scenario];
    var aggregate = {};

    if (collection === undefined) {
        return res.send(404, 'Collection not found');
    }

    if (!_.has(req.query, 'start') || !_.has(req.query, 'end')) {
        return res.send(400, '"start" and "end" query parameters required');
    }

    // start && end parameter constraints
    if (!core.REGEX.iso8601.test(req.query.start) || !core.REGEX.iso8601.test(req.query.end)) {
        return res.send(400, 'Bad Request');
    }

    numeric.precision = core.PRECISION;

    // T Aggregation in Space or Time
    // ------------------------------

    if (_.has(req.query, 'aggregate')) {
        return res.send(404, 'Aggregate queries are not yet implemented');

    } else if (_.has(req.query, 'geom')) {
        core.checkGeometryCollection(req, res, core.runGeomQuery);

    } else {
        return res.send(400, 'Bad Request');

    }

};

exports.roi = roi;

<commit_msg>Minor error message modificaiton<commit_after>var core = require('../core').core;
var moment = require('../node_modules/moment/moment');
var numeric = require('numeric');
var _ = require('underscore');

// GET Parameters:
//
//     aggregate   [Optional]  Used with: interval, start, end, geom
//     end         [Optional]  Used with: aggregate, interval, start, geom
//     geom        [Optional]  Used with: aggregate, start, end
//     interval    [Optional]  Used with: aggregate, start, end
//     start       [Optional]  Used with: aggregate, interval, end, geom
//
//     Valid combinations:
//       (geom, start, end)
//       (geom, start, end, aggregate)
//       (geom, start, end, aggregate, interval),

function roi (req, res) {
    var body, coords, grouping, i, idx, projection;
    var collection = core.DATA[req.params.scenario];
    var aggregate = {};

    if (collection === undefined) {
        return res.send(404, 'Collection not found');
    }

    if (!_.has(req.query, 'start') || !_.has(req.query, 'end')) {
        return res.send(400, '"start" and "end" query parameters required');
    }

    // start && end parameter constraints
    if (!core.REGEX.iso8601.test(req.query.start) || !core.REGEX.iso8601.test(req.query.end)) {
        return res.send(400, 'Bad Request');
    }

    numeric.precision = core.PRECISION;

    // T Aggregation in Space or Time
    // ------------------------------

    if (_.has(req.query, 'aggregate')) {
        return res.send(404, 'Aggregate queries are not yet supported for ROI statistics');

    } else if (_.has(req.query, 'geom')) {
        core.checkGeometryCollection(req, res, core.runGeomQuery);

    } else {
        return res.send(400, 'Bad Request');

    }

};

exports.roi = roi;

<|endoftext|>


<commit_before>'use strict';

const EventEmitter = require('events');
const dgram = require('dgram');

const Device = require('../device');
const Sensor = require('./capabilities/sensor');

const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';

const SubDevice = require('./gateway/subdevice');
const devices = {
	// Temperature and Humidity sensor
	'sensor_ht': require('./gateway/sensor_ht'),

	// Switch (button)
	'switch': require('./gateway/switch'),
	// Cube controller
	'cube': require('./gateway/cube'),
	// Light Switches (1 and 2 buttons)
	'86sw1': require('./gateway/86sw1'),
	'86sw2': require('./gateway/86sw2'),

	// Motion sensor
	'motion': require('./gateway/motion'),
	// Magnet sensor for windows and doors
	'magnet': require('./gateway/magnet'),

	// Simple plug for sockets
	'plug': require('./gateway/plug'),

	// Plugs for light switches
	'ctrl_neutral1': require('./gateway/ctrl_neutral1'),
	'ctrl_neutral2': require('./gateway/ctrl_neutral2')
};

/**
 * Generate a key for use with the local developer API. This does not generate
 * a secure key, but the Mi Home app does not seem to do that either.
 */
function generateKey() {
	let result = '';
	for(let i=0; i<16; i++) {
		let idx = Math.floor(Math.random() * CHARS.length)
		result += CHARS[idx];
	}
	return result;
}

/**
 * Abstraction over a Mi Smart Home Gateway.
 *
 * This device is very different from most other miIO-devices in that it has
 * devices on its own. There seems to be a way to talk to these devices by
 * setting `sid` in the requests, which means we can poll them. The problem
 * is that certain devices also send events, but these seem to only be
 * accessible over a local developer connection.
 *
 * So to work around this we activate the local developer connection and use
 * that together with the regular calls for changing things on the gateway.
 */
class Gateway extends Device {
	static get TYPE() { return 'gateway' }

	static get SubDevice() { return SubDevice }

	static registerSubDevice(id, deviceClass) {
		devices[id] = deviceClass;
	}

	constructor(options) {
		super(options);

		this.type = Gateway.TYPE;

		this._devices = {};
	}

	_report(properties) {
		Object.keys(properties).forEach(key => {
			switch(key) {
				case 'illumination':
					// TODO: What scale does this actually have? Lux?
					this.setProperty('illuminance', properties[key]);
					break;
				case 'rgb':
				{
					const rgba = properties[key];

					this.setProperty('rgb',  {
						red: 0xff & (rgba >> 16),
						green: 0xff & (rgba >> 8),
						blue: 0xff & rgba
					});
					this.setProperty('brightness', Math.round((0xff & (rgba >> 24)) / 255 * 100));
					break;
				}
			}
		});
	}

	_setDeveloperKey(key) {
		this._developerKey = key;

		// If we are already connected to the Developer API skip init
		if(this.devApi) return Promise.resolve();

		return new Promise((resolve, reject) => {
			this.devApi = new DeveloperApi(this, this.address);

			this.devApi.on('deviceAvailable', deviceInfo => {
				const currentDevice = this._devices[deviceInfo.id];
				if(currentDevice) {
					currentDevice.destroy();
					this.emit('deviceUnavailable', currentDevice);
				}

				const type = devices[deviceInfo.model] || SubDevice;
				const device = this._devices[deviceInfo.id] = new type(this, deviceInfo);
				device._report(deviceInfo);
				this.emit('deviceAvailable', device);
			});
			this.devApi.on('deviceUpdated', deviceInfo => {
				const currentDevice = this._devices[deviceInfo.id];
				if(! currentDevice) return;

				currentDevice._report(deviceInfo);
			});
			this.devApi.on('properties', this._report.bind(this));
			this.devApi.on('ready', resolve);

			setTimeout(reject, 10000);

			// TODO: Device removal
		});
	}

	init() {
		return super.init()
			.then(() => this._findDeveloperKey())
			.then(() => this._mixins())
			.then(() => this);
	}

	_mixins() {
		if(this.property('rgb')) {
			this._mixinLight()
		}

		const illuminance = this.property('illuminance');
		if(typeof illuminance !== 'undefined' && illuminance !== null) {
			Sensor.extend(this, { name: 'illuminance' });
		}
	}

	_mixinLight() {
		this.capabilities.push('color:rgb');
		this.setRGB = function(color) {
			if(typeof color === 'string') {
				const parsed = /#?(([0-9a-f]{6})|([0-9a-f]{3}))/i.exec(color);
				if(! parsed) {
					throw new Error('Invalid hex color: ' + color);
				}

				const rgb = parsed[1];
				if(rgb.length == 3) {
					color = {
						red: parseInt(rgb[0] + rgb[0], 16),
						green: parseInt(rgb[1] + rgb[1], 16),
						blue: parseInt(rgb[2] + rgb[2], 16),
					};
				} else {
					color = {
						red: parseInt(rgb.substring(0, 2), 16),
						green: parseInt(rgb.substring(2, 4), 16),
						blue: parseInt(rgb.substring(4, 6), 16)
					};
				}
			}

			const a = Math.max(0, Math.min(255, Math.round(this.property('brightness') / 100 * 255)));
			const rgb = a << 24 | (color.red << 16) | (color.green << 8) | color.blue;
			return this.call('set_rgb', [ rgb >>> 0 ]);//, { refresh: [ 'rgb' ] });
		};

		Object.defineProperty(this, 'rgb', {
			get: function() { return this.property('rgb') }
		});

		this.capabilities.push('brightness');
		this.setBrightness = function(brightness) {
			const a = Math.max(0, Math.min(255, Math.round(brightness / 100 * 255)));
			const color = this.property('rgb');
			const rgb = a << 24 | (color.red << 16) | (color.green << 8) | color.blue;
			return this.call('set_rgb', [ rgb >>> 0 ]);//, { refresh: [ 'rgb' ] });
		};

		Object.defineProperty(this, 'brightness', {
			get: function() { return this.property('brightness') }
		});
	}

	get devices() {
		const result = [];
		Object.keys(this._devices).forEach(key => result.push(this._devices[key]));
		return result;
	}

	/**
	 * Either resolve the developer key or setup a new one.
	 */
	_findDeveloperKey() {
		// We already have a developer key
		if(this._developerKey) return Promise.resolve(this._developerKey);

		// Call and fetch the key or set a new key
		return this.call('get_lumi_dpf_aes_key')
			.then(result => {
				let key = result[0];
				if(key && key.length === 16) {
					// This is a valid key, store it
					return key;
				}

				key = generateKey();
				return this.call('set_lumi_dpf_aes_key', [ key ]);
			})
			.then(key => this._setDeveloperKey(key));
	}

	/**
	 * Start the process of adding a device. Allows Zigbee devices to join for
	 * 30 seconds.
	 */
	addDevice() {
		return this.call('start_zigbee_join', [ 30 ])
			.then(result => result[0] == 'ok');
	}

	/**
	 * Stop allowing devices to join.
	 */
	stopAddDevice() {
		return this.call('start_zigbee_join', [ 0 ])
			.then(result => result[0] == 'ok');
	}

	/**
	 * Remove a device from the gateway using the identifier of the device.
	 */
	removeDevice(id) {
		return this.call('remove_device', [ id ]);
	}

	destroy() {
		super.destroy();

		if(this.net) {
			this.net.destroy();
		}
	}
}

const MULTICAST_ADDRESS = '224.0.0.50';
const MULTICAST_PORT = 4321;

/**
 * Local Developer API for the gateway. Used to read data from the gateway
 * and connected devices.
 *
 * TODO: Retries for discovery
 */
class DeveloperApi extends EventEmitter {
	constructor(parent, address) {
		super();

		this.address = address;
		this.debug = parent.debug;

		this.socket = dgram.createSocket({
			type: 'udp4',
			reuseAddr: true
		});

		this.socket.on('message', this._onMessage.bind(this));
		this.socket.on('listening', () => {
			this.socket.addMembership(MULTICAST_ADDRESS);

			this.send({
				cmd: 'whois'
			});
		});
		this.socket.bind(9898);

		this.devices = [];
		this.ready = false;
	}

	destroy() {
		this.socket.destroy();
	}

	send(data) {
		const json = JSON.stringify(data);
		this.debug('DEV ->', json);
		this.socket.send(json, 0, json.length, this.port || MULTICAST_PORT, this.address);
	}

	_onMessage(msg) {
		let data;
		try {
			this.debug('DEV <-', msg.toString());
			data = JSON.parse(msg.toString());
		} catch(ex) {
			this.emit('error', ex);
			return;
		}

		switch(data.cmd) {
			case 'iam':
			// TODO: Do we need to protect against multiple gateways?

				// This is a response to the initial whois
				this.sid = data.sid;
				this.port = data.port;

				// Discover devices
				this.send({
					cmd: 'get_id_list'
				});

				break;
			case 'heartbeat':
				if(data.sid === this.sid) {
					this.token = data.token;
				}
				break;
			case 'get_id_list_ack':
				if(data.sid === this.sid) {
					// Response with all identifiers of the devices
					this.token = data.token;
					const devices = JSON.parse(data.data);

					// Read data for ourself
					this.send({
						cmd: 'read',
						sid: this.sid
					});

					const now = Date.now();
					devices.forEach(id => {
						this.devices[id] = {
							id: id,
							lastSeen: now
						};

						this.send({
							cmd: 'read',
							sid: id
						});
					});
				}
				break;
			case 'read_ack': {
				const device = this.devices[data.sid];
				if(device) {
					device.lastSeen = Date.now();
					device.data = JSON.parse(data.data);

					if(! device.model) {
						// This is the first read
						device.model = data.model;
						this.emit('deviceAvailable', device);
					}
				} else if(data.sid === this.sid) {
					this.emit('properties', JSON.parse(data.data));

					if(! this.ready) {
						// TODO: Wait for reads from all devices
						this.emit('ready');
						this.ready = true;
					}
				}
				break;
			}
			case 'report': {
				const device = this.devices[data.sid];
				if(device) {
					device.lastSeen = Date.now();

					const parsed = JSON.parse(data.data);
					Object.keys(parsed).forEach(key => {
						device.data[key] = parsed[key];
					});

					this.emit('deviceUpdated', device);
				} else if(data.sid === this.sid) {
					this.emit('properties', JSON.parse(data.data));
				}
			}
		}
	}
}

module.exports = Gateway;
<commit_msg>Actual brightness limit is 100, no need to scale<commit_after>'use strict';

const EventEmitter = require('events');
const dgram = require('dgram');

const Device = require('../device');
const Sensor = require('./capabilities/sensor');

const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';

const SubDevice = require('./gateway/subdevice');
const devices = {
	// Temperature and Humidity sensor
	'sensor_ht': require('./gateway/sensor_ht'),

	// Switch (button)
	'switch': require('./gateway/switch'),
	// Cube controller
	'cube': require('./gateway/cube'),
	// Light Switches (1 and 2 buttons)
	'86sw1': require('./gateway/86sw1'),
	'86sw2': require('./gateway/86sw2'),

	// Motion sensor
	'motion': require('./gateway/motion'),
	// Magnet sensor for windows and doors
	'magnet': require('./gateway/magnet'),

	// Simple plug for sockets
	'plug': require('./gateway/plug'),

	// Plugs for light switches
	'ctrl_neutral1': require('./gateway/ctrl_neutral1'),
	'ctrl_neutral2': require('./gateway/ctrl_neutral2')
};

/**
 * Generate a key for use with the local developer API. This does not generate
 * a secure key, but the Mi Home app does not seem to do that either.
 */
function generateKey() {
	let result = '';
	for(let i=0; i<16; i++) {
		let idx = Math.floor(Math.random() * CHARS.length)
		result += CHARS[idx];
	}
	return result;
}

/**
 * Abstraction over a Mi Smart Home Gateway.
 *
 * This device is very different from most other miIO-devices in that it has
 * devices on its own. There seems to be a way to talk to these devices by
 * setting `sid` in the requests, which means we can poll them. The problem
 * is that certain devices also send events, but these seem to only be
 * accessible over a local developer connection.
 *
 * So to work around this we activate the local developer connection and use
 * that together with the regular calls for changing things on the gateway.
 */
class Gateway extends Device {
	static get TYPE() { return 'gateway' }

	static get SubDevice() { return SubDevice }

	static registerSubDevice(id, deviceClass) {
		devices[id] = deviceClass;
	}

	constructor(options) {
		super(options);

		this.type = Gateway.TYPE;

		this._devices = {};
	}

	_report(properties) {
		Object.keys(properties).forEach(key => {
			switch(key) {
				case 'illumination':
					// TODO: What scale does this actually have? Lux?
					this.setProperty('illuminance', properties[key]);
					break;
				case 'rgb':
				{
					const rgba = properties[key];

					this.setProperty('rgb',  {
						red: 0xff & (rgba >> 16),
						green: 0xff & (rgba >> 8),
						blue: 0xff & rgba
					});
					this.setProperty('brightness', Math.round((0xff & (rgba >> 24)) ));
					break;
				}
			}
		});
	}

	_setDeveloperKey(key) {
		this._developerKey = key;

		// If we are already connected to the Developer API skip init
		if(this.devApi) return Promise.resolve();

		return new Promise((resolve, reject) => {
			this.devApi = new DeveloperApi(this, this.address);

			this.devApi.on('deviceAvailable', deviceInfo => {
				const currentDevice = this._devices[deviceInfo.id];
				if(currentDevice) {
					currentDevice.destroy();
					this.emit('deviceUnavailable', currentDevice);
				}

				const type = devices[deviceInfo.model] || SubDevice;
				const device = this._devices[deviceInfo.id] = new type(this, deviceInfo);
				device._report(deviceInfo);
				this.emit('deviceAvailable', device);
			});
			this.devApi.on('deviceUpdated', deviceInfo => {
				const currentDevice = this._devices[deviceInfo.id];
				if(! currentDevice) return;

				currentDevice._report(deviceInfo);
			});
			this.devApi.on('properties', this._report.bind(this));
			this.devApi.on('ready', resolve);

			setTimeout(reject, 10000);

			// TODO: Device removal
		});
	}

	init() {
		return super.init()
			.then(() => this._findDeveloperKey())
			.then(() => this._mixins())
			.then(() => this);
	}

	_mixins() {
		if(this.property('rgb')) {
			this._mixinLight()
		}

		const illuminance = this.property('illuminance');
		if(typeof illuminance !== 'undefined' && illuminance !== null) {
			Sensor.extend(this, { name: 'illuminance' });
		}
	}

	_mixinLight() {
		this.capabilities.push('color:rgb');
		this.setRGB = function(color) {
			if(typeof color === 'string') {
				const parsed = /#?(([0-9a-f]{6})|([0-9a-f]{3}))/i.exec(color);
				if(! parsed) {
					throw new Error('Invalid hex color: ' + color);
				}

				const rgb = parsed[1];
				if(rgb.length == 3) {
					color = {
						red: parseInt(rgb[0] + rgb[0], 16),
						green: parseInt(rgb[1] + rgb[1], 16),
						blue: parseInt(rgb[2] + rgb[2], 16),
					};
				} else {
					color = {
						red: parseInt(rgb.substring(0, 2), 16),
						green: parseInt(rgb.substring(2, 4), 16),
						blue: parseInt(rgb.substring(4, 6), 16)
					};
				}
			}

			const a = Math.max(0, Math.min(255, Math.round(this.property('brightness'))));
			const rgb = a << 24 | (color.red << 16) | (color.green << 8) | color.blue;
			return this.call('set_rgb', [ rgb >>> 0 ]);//, { refresh: [ 'rgb' ] });
		};

		Object.defineProperty(this, 'rgb', {
			get: function() { return this.property('rgb') }
		});

		this.capabilities.push('brightness');
		this.setBrightness = function(brightness) {
			const a = Math.max(0, Math.min(255, Math.round(brightness)));
			const color = this.property('rgb');
			const rgb = a << 24 | (color.red << 16) | (color.green << 8) | color.blue;
			return this.call('set_rgb', [ rgb >>> 0 ]);//, { refresh: [ 'rgb' ] });
		};

		Object.defineProperty(this, 'brightness', {
			get: function() { return this.property('brightness') }
		});
	}

	get devices() {
		const result = [];
		Object.keys(this._devices).forEach(key => result.push(this._devices[key]));
		return result;
	}

	/**
	 * Either resolve the developer key or setup a new one.
	 */
	_findDeveloperKey() {
		// We already have a developer key
		if(this._developerKey) return Promise.resolve(this._developerKey);

		// Call and fetch the key or set a new key
		return this.call('get_lumi_dpf_aes_key')
			.then(result => {
				let key = result[0];
				if(key && key.length === 16) {
					// This is a valid key, store it
					return key;
				}

				key = generateKey();
				return this.call('set_lumi_dpf_aes_key', [ key ]);
			})
			.then(key => this._setDeveloperKey(key));
	}

	/**
	 * Start the process of adding a device. Allows Zigbee devices to join for
	 * 30 seconds.
	 */
	addDevice() {
		return this.call('start_zigbee_join', [ 30 ])
			.then(result => result[0] == 'ok');
	}

	/**
	 * Stop allowing devices to join.
	 */
	stopAddDevice() {
		return this.call('start_zigbee_join', [ 0 ])
			.then(result => result[0] == 'ok');
	}

	/**
	 * Remove a device from the gateway using the identifier of the device.
	 */
	removeDevice(id) {
		return this.call('remove_device', [ id ]);
	}

	destroy() {
		super.destroy();

		if(this.net) {
			this.net.destroy();
		}
	}
}

const MULTICAST_ADDRESS = '224.0.0.50';
const MULTICAST_PORT = 4321;

/**
 * Local Developer API for the gateway. Used to read data from the gateway
 * and connected devices.
 *
 * TODO: Retries for discovery
 */
class DeveloperApi extends EventEmitter {
	constructor(parent, address) {
		super();

		this.address = address;
		this.debug = parent.debug;

		this.socket = dgram.createSocket({
			type: 'udp4',
			reuseAddr: true
		});

		this.socket.on('message', this._onMessage.bind(this));
		this.socket.on('listening', () => {
			this.socket.addMembership(MULTICAST_ADDRESS);

			this.send({
				cmd: 'whois'
			});
		});
		this.socket.bind(9898);

		this.devices = [];
		this.ready = false;
	}

	destroy() {
		this.socket.destroy();
	}

	send(data) {
		const json = JSON.stringify(data);
		this.debug('DEV ->', json);
		this.socket.send(json, 0, json.length, this.port || MULTICAST_PORT, this.address);
	}

	_onMessage(msg) {
		let data;
		try {
			this.debug('DEV <-', msg.toString());
			data = JSON.parse(msg.toString());
		} catch(ex) {
			this.emit('error', ex);
			return;
		}

		switch(data.cmd) {
			case 'iam':
			// TODO: Do we need to protect against multiple gateways?

				// This is a response to the initial whois
				this.sid = data.sid;
				this.port = data.port;

				// Discover devices
				this.send({
					cmd: 'get_id_list'
				});

				break;
			case 'heartbeat':
				if(data.sid === this.sid) {
					this.token = data.token;
				}
				break;
			case 'get_id_list_ack':
				if(data.sid === this.sid) {
					// Response with all identifiers of the devices
					this.token = data.token;
					const devices = JSON.parse(data.data);

					// Read data for ourself
					this.send({
						cmd: 'read',
						sid: this.sid
					});

					const now = Date.now();
					devices.forEach(id => {
						this.devices[id] = {
							id: id,
							lastSeen: now
						};

						this.send({
							cmd: 'read',
							sid: id
						});
					});
				}
				break;
			case 'read_ack': {
				const device = this.devices[data.sid];
				if(device) {
					device.lastSeen = Date.now();
					device.data = JSON.parse(data.data);

					if(! device.model) {
						// This is the first read
						device.model = data.model;
						this.emit('deviceAvailable', device);
					}
				} else if(data.sid === this.sid) {
					this.emit('properties', JSON.parse(data.data));

					if(! this.ready) {
						// TODO: Wait for reads from all devices
						this.emit('ready');
						this.ready = true;
					}
				}
				break;
			}
			case 'report': {
				const device = this.devices[data.sid];
				if(device) {
					device.lastSeen = Date.now();

					const parsed = JSON.parse(data.data);
					Object.keys(parsed).forEach(key => {
						device.data[key] = parsed[key];
					});

					this.emit('deviceUpdated', device);
				} else if(data.sid === this.sid) {
					this.emit('properties', JSON.parse(data.data));
				}
			}
		}
	}
}

module.exports = Gateway;
<|endoftext|>


<commit_before>var Rdio = require("node-rdio");
var config = require('../config/config.json');

var rdio = new Rdio([config.rdio.key, config.rdio.secret]);

var parseUrlForName = function(json) {
  if (json && json.result) {
    return json.result.name;
  } else {
    return false;
  }
};

var parseSearchForUrl = function(json) {
  if (json && json.result.results[0]) {
    return json.result.results[0].shortUrl;
  } else {
    return false;
  }
};

exports.getTrackName = function(short_code, cb) {
  rdio.call('getObjectFromShortCode', {'short_code': short_code}, function(err, data) {
    if (err) {
      return cb(err, {});
    }
    return cb(err, parseUrlForName(data));
  });
};

exports.getTrackUrl = function(name, cb) {
  rdio.call('search', {'query' : name, 'types': 'Tracks'}, function(err, data) {
    if (err) {
      return cb(err, {});
    }
    return cb(err, parseSearchForUrl(data));
  });
};
<commit_msg>add environment keys<commit_after>var Rdio = require("node-rdio");
var config = require('../config/config.json');

console.log(process.env);

var rdio = new Rdio([process.env['RDIO:KEY'], process.env['RDIO:SECRET']]);

var parseUrlForName = function(json) {
  if (json && json.result) {
    return json.result.name;
  } else {
    return false;
  }
};

var parseSearchForUrl = function(json) {
  if (json && json.result.results[0]) {
    return json.result.results[0].shortUrl;
  } else {
    return false;
  }
};

exports.getTrackName = function(short_code, cb) {
  rdio.call('getObjectFromShortCode', {'short_code': short_code}, function(err, data) {
    if (err) {
      return cb(err, {});
    }
    return cb(err, parseUrlForName(data));
  });
};

exports.getTrackUrl = function(name, cb) {
  rdio.call('search', {'query' : name, 'types': 'Tracks'}, function(err, data) {
    if (err) {
      return cb(err, {});
    }
    return cb(err, parseSearchForUrl(data));
  });
};
<|endoftext|>


<commit_before>$(document).ready(function () {
    var body_height = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
    var body_width = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth;
    $('.body').height(body_height - 100);

    //Create scaled canvas element
    var vert_percent = 1;
    var horz_percent = 1;
    var canv_width = horz_percent * body_width;
    var canv_height = vert_percent * (body_height - 100);
    var canv_def = "<canvas id='can' height='" + canv_height + "' width='" + canv_width + "'>Sorry, your browser does not support html5:(</canvas>";
    $('.body').append(canv_def);

    var ctx = document.getElementById('can').getContext('2d');
    var pressed = {}; //Used for getting user inputs
    var Circle_array = [];
    var friction = 1.1;
    var delta_t = 0.05;
    var max_veloc = 20;
    var count = -1; //Used as a unique ID for each circle
    var deletions = 0; //Used to modify the unique ID when deleting an element

    function Circle(x, y, r, color, count) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.color = color;
	this.color.push(1);
        this.x_veloc = 0;
        this.y_veloc = 0;
	this.count = count;

        this.draw = function () {
            ctx.fillStyle = 'rgba('+this.color.join(',')+')';
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
            ctx.fill();
            ctx.linewidth = 1;
            ctx.fillStyle = "black";
            ctx.stroke();
            ctx.closePath();
        };

        this.updatePosition = function (scale) {
	    //Update velocities
            this.x_veloc += ((scale * Math.random()) - (scale / 2)) / this.r;
            this.y_veloc += ((scale * Math.random()) - (scale / 2)) / this.r;
            this.x_veloc /= friction;
            this.y_veloc /= friction;
            this.x_veloc = Math.min(this.x_veloc, max_veloc);
            this.y_veloc = Math.min(this.y_veloc, max_veloc);
	    //Update positions - make sure dot stays on canvas
            this.x += this.x_veloc * delta_t;
            this.y += this.y_veloc * delta_t;
            if (this.x > canv_width) {
                this.x %= canv_width;
            } else if (this.x < 0) {
                this.x = canv_width;
            }
            if (this.y > canv_height) {
                this.y %= canv_height;
            } else if (this.y < 0) {
                this.y = canv_height;
            }
	    //Update alpha
	    this.alpha *= .988514; //Sets half-life to about 1 second (60 frames, hopefully...)
        };
    }

    function getRGB() {
	return [Math.floor(Math.random()*256),Math.floor(Math.random()*256),Math.floor(Math.random()*256)];
    }

    function deleteCircle(circle) {
	Cirlce_array.splice(circle.count-deletions,1);
	deletions++;
    }

    function setup() {
        document.addEventListener('keydown', function (e) {
            pressed[e.keyCode] = true;
        });
        document.addEventListener('keyup', function (e) {
            pressed[e.keyCode] = false;
        });
    }

    function updateGameState() {
        if (pressed[' '.charCodeAt(0)] == true) {
	    count++;
            var x = Math.random() * canv_width;
            var y = Math.random() * canv_height;
            var r = Math.random() * 35 + 3;
            Circle_array.push(new Circle(x, y, r, getRGB(), count))
        }
        for (i = 0; i < Circle_array.length; i++) {
	    Circle_array[i].updatePosition();
        }
    }

    function draw() {
        ctx.clearRect(0, 0, canv_width, canv_height);
        for (i = 0; i < Circle_array.length; i++) {
	    if(Circle_array[i].alpha < .01) {
		deleteCircle(Circle_array[i]);
	    }
	    else {
		Circle_array[i].draw();
		console.log(Circle_array[i].color+','+Circle_array[i].count);
	    }
        }
    }

    function gameLoop() {
        updateGameState();
        draw();
        window.requestAnimFrame(gameLoop);
    }

    window.requestAnimFrame = (function () {
        return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) {
            window.setTimeout(callback, 1000 / 60);
        };
    })();

    setup();
    window.requestAnimFrame(gameLoop);
});
<commit_msg>A basic life-cycle which consists of slowly increasing transparency until the circle dissapears (and gets deleted from Circle_array). There are a couple of kinks to work out, however. First, which elements are being removed from the array appears to be buggy. Second, there is not always a smooth transition when a bubble is deleted - it's transparency isn't low enough when the delete happens. Third, it would be nice if the black circles around the bubbles were also increasingly transparent, but I couldn't get rgba to work when drawing the outlines.<commit_after>$(document).ready(function () {
    var body_height = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
    var body_width = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth;
    $('.body').height(body_height - 100);

    //Create scaled canvas element
    var vert_percent = 1;
    var horz_percent = 1;
    var canv_width = horz_percent * body_width;
    var canv_height = vert_percent * (body_height - 100);
    var canv_def = "<canvas id='can' height='" + canv_height + "' width='" + canv_width + "'>Sorry, your browser does not support html5:(</canvas>";
    $('.body').append(canv_def);

    var ctx = document.getElementById('can').getContext('2d');
    var pressed = {}; //Used for getting user inputs
    var Circle_array = [];
    var friction = 1.0;
    var delta_t = 0.05;
    var max_veloc = 20;
    var ID = -1; //Used as a unique ID for each circle
    var deletions = 0; //Used to modify the unique ID when deleting an element

    function deleteElem(obj,array) {
	console.log('Deleted object #:'+obj.ID);
	array.splice(obj.ID-deletions,1);
	deletions++;
    }

    //Return an integer 0 to max (inclusive)
    function getRandInt(max) {
	return Math.floor(Math.random()*max);
    }

    //Return a real min to max (inclusive)
    function getUnif(min,max) {
	return Math.random()*(max-min) + min;
    }

    //Emulate a normal distribution - this uses the central limit theorem (the more iterations, the closer to a true normal it will be)
    function getNorm(mean,iterations) {
	var output = 0;
	for (i = 0; i < iterations; i++) {
	    output += Math.random()*mean;
	}
	return output/iterations;
    }

    function setup() {
        document.addEventListener('keydown', function (e) {
            pressed[e.keyCode] = true;
        });
        document.addEventListener('keyup', function (e) {
            pressed[e.keyCode] = false;
        });
    }

    function Circle(x, y, r, red, green, blue, ID) {
        this.x = x;
        this.y = y;
        this.r = r;
	this.color = [];
	this.color.push(red);
	this.color.push(green);
	this.color.push(blue);
	this.alpha = 1;
        this.x_veloc = 0;
        this.y_veloc = 0;
	this.ID = ID;
	this.maxAge = Math.ceil(getNorm(20*60,6)) + 600; //Age measured in frames
	this.age = 0;

        this.draw = function () {
            ctx.fillStyle = 'rgba('+this.color.join(',')+','+this.alpha+')';
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
            ctx.fill();
            //ctx.linewidth = 1;
            //ctx.fillStyle = 'rgba(1,1,1,'+this.alpha+')';
            //ctx.stroke();
            //ctx.closePath();
        };

        this.updatePosition = function (scale) {
	    //Update velocities
            this.x_veloc += getUnif(-scale,scale) / this.r;
            this.y_veloc += getUnif(-scale,scale) / this.r;
            this.x_veloc /= friction;
            this.y_veloc /= friction;
            this.x_veloc = Math.min(this.x_veloc, max_veloc);
            this.y_veloc = Math.min(this.y_veloc, max_veloc);
	    //Update positions - make sure dot stays on canvas
            this.x += this.x_veloc * delta_t;
            this.y += this.y_veloc * delta_t;
            if (this.x > canv_width) {
                this.x %= canv_width;
            } else if (this.x < 0) {
                this.x = canv_width;
            }
            if (this.y > canv_height) {
                this.y %= canv_height;
            } else if (this.y < 0) {
                this.y = canv_height;
            }
	    this.age++;
	    this.alpha -= (1/this.maxAge);
        };
    }

    function updateGameState() {
        if (pressed[' '.charCodeAt(0)] == true) {
	    ID++;
            Circle_array.push(new Circle(getUnif(0,canv_width), getUnif(0,canv_height), getUnif(3,35), getRandInt(255), getRandInt(255), getRandInt(255), ID));
        }
        for (i = 0; i < Circle_array.length; i++) {
	    if((Circle_array.length > 1) && (Circle_array[i].age === Circle_array[i].maxAge)) {
	    	deleteElem(Circle_array[i],Circle_array);
	    }
	    else {
		Circle_array[i].updatePosition(15);
	    }
        }
    }

    function draw() {
        ctx.clearRect(0, 0, canv_width, canv_height);
        for (i = 0; i < Circle_array.length; i++) {
	    Circle_array[i].draw();
	    console.log(Circle_array[i].color+','+Circle_array[i].age+','+Circle_array[i].maxAge);
        }
    }

    function gameLoop() {
        updateGameState();
        draw();
        window.requestAnimFrame(gameLoop);
    }

    window.requestAnimFrame = (function () {
        return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) {
            window.setTimeout(callback, 1000 / 60);
        };
    })();

    setup();
    window.requestAnimFrame(gameLoop);
});
<|endoftext|>


<commit_before>import React from 'react'
import { PropTypes } from 'react'
const { string, bool, func } = PropTypes

export default class Checkbox extends React.Component {
  static defaultProps = {
    className: 'Checkbox',
    value: '',
    handleChange: () => {},
  }

  static propTypes = {
    id: string.isRequired,
    labelText: string.isRequired,
    name: string.isRequired,
    ariaDescribedby: string,
    className: string,
    selected: bool,
    handleChange: func,
  }

  state = {
    selected: this.props.selected,
  }

  render() {
    const { className, ariaDescribedby, id, labelText, name } = this.props
    return (
      <div className={`${className} Checkbox`}>
        <input
          aria-describedby={ariaDescribedby}
          className={`${className}__text Checkbox__text`}
          id={id}
          name={name}
          defaultChecked={this.state.selected}
          onChange={this._handleChange}
          type="checkbox"
        />
        <label className={`${className}__label Checkbox__label`} htmlFor={id}>
          {labelText}

    )
  }
  _handleChange = () => {
    const newValue = !this.state.selected
    this.setState({ selected: newValue }, () => {
      this.props.handleChange(newValue)
    })
  }
}
<commit_msg>Checkbox: checked defaults to value={true}<commit_after>import React from 'react'
import { PropTypes } from 'react'
const { string, bool, func, oneOfType } = PropTypes

export default class Checkbox extends React.Component {
  static defaultProps = {
    className: 'Checkbox',
    value: true,
    handleChange: () => {},
  }

  static propTypes = {
    id: string.isRequired,
    labelText: string.isRequired,
    name: string.isRequired,
    ariaDescribedby: string,
    className: string,
    selected: bool,
    handleChange: func,
    value: oneOfType(string, bool),
  }

  state = {
    selected: this.props.selected,
  }

  render() {
    const { className, ariaDescribedby, id, labelText, name, value } = this.props
    return (
      <div className={`${className} Checkbox`}>
        <input
          aria-describedby={ariaDescribedby}
          className={`${className}__text Checkbox__text`}
          id={id}
          name={name}
          defaultChecked={this.state.selected}
          onChange={this._handleChange}
          value={value}
          type="checkbox"
        />
        <label className={`${className}__label Checkbox__label`} htmlFor={id}>
          {labelText}

    )
  }
  _handleChange = () => {
    const newValue = !this.state.selected
    this.setState({ selected: newValue }, () => {
      this.props.handleChange(newValue)
    })
  }
}
<|endoftext|>


<commit_before>'use strict';

module.exports = (srcPath) => {
  const Broadcast = require(srcPath + 'Broadcast');

  return {
    usage: 'quit',
    command: (state) => (args, player) => {
      if (player.isInCombat()) {
        Broadcast.sayAt(player, "You're too busy fighting for your life!");
      }

      player.save(() => {
        Broadcast.sayAt(player, "Goodbye!");
        Broadcast.sayAt(player.room, `${player.name} disappears.`);
        player.socket.emit('close');
      });
    }
  };
};
<commit_msg>Return out of command to disallow quitting during a fight. (#208)<commit_after>'use strict';

module.exports = (srcPath) => {
  const Broadcast = require(srcPath + 'Broadcast');

  return {
    usage: 'quit',
    command: (state) => (args, player) => {
      if (player.isInCombat()) {
        return Broadcast.sayAt(player, "You're too busy fighting for your life!");
      }

      player.save(() => {
        Broadcast.sayAt(player, "Goodbye!");
        Broadcast.sayAt(player.room, `${player.name} disappears.`);
        player.socket.emit('close');
      });
    }
  };
};
<|endoftext|>