<commit_before>'use strict';
var io = require('socket.io-client');
const Serializer = require('./serialize/Serializer');
const NetworkTransmitter = require('./network/NetworkTransmitter');
const NetworkMonitor = require('./network/NetworkMonitor');
const Synchronizer = require('./Synchronizer');
const Scheduler = require('./lib/Scheduler');

// externalizing these parameters as options would add confusion to game
// developers, and provide no real benefit.
const STEP_DRIFT_THRESHOLD = 10; // min # steps that qualifies a client-server drift
const GAME_UPS = 60; // default number of game steps per second
const STEP_DELAY_MSEC = 5; // if drift detected, delay next execution by this amount

/**
 * The client engine is the singleton which manages the client-side
 * process, starting the game engine, listening to network messages,
 * starting client steps, and handling world updates which arrive from
 * the server.
 */
class ClientEngine {

    /**
      * Create a client engine instance.
      *
      * @param {GameEngine} gameEngine - a game engine
      * @param {Object} inputOptions - options object
      * @param {Boolean} inputOptions.autoConnect - if true, the client will automatically attempt connect to server.
      * @param {Number} inputOptions.delayInputCount - if set, inputs will be delayed by this many steps before they are actually applied on the client.
      * @param {Number} inputOptions.healthCheckInterval - health check message interval (millisec).  Default is 1000.
      * @param {Number} inputOptions.healthCheckRTTSample - health check RTT calculation sample size.  Default is 10.
      * @param {Object} inputOptions.syncOptions - an object describing the synchronization method.  If not set, will be set to extrapolate, with local object bending set to 0.0 and remote object bending set to 0.6.  If the query-string parameter "sync" is defined, then that value is passed to this object's sync attribute.
      * @param {String} inputOptions.syncOptions.sync - chosen sync option, can be interpolate, extrapolate, or frameSync
      * @param {Number} inputOptions.syncOptions.localObjBending - amount of bending towards original client position, after each sync, for local objects
      * @param {Number} inputOptions.syncOptions.remoteObjBending - amount of bending towards original client position, after each sync, for remote objects
      * @param {Renderer} Renderer - the Renderer class constructor
      */
    constructor(gameEngine, inputOptions, Renderer) {

        this.options = Object.assign({
            autoConnect: true,
            healthCheckInterval: 1000,
            healthCheckRTTSample: 10,
            stepPeriod: 1000 / GAME_UPS
        }, inputOptions);

        /**
         * reference to serializer
         * @member {Serializer}
         */
        this.serializer = new Serializer();

        /**
         * reference to game engine
         * @member {GameEngine}
         */
        this.gameEngine = gameEngine;
        this.networkTransmitter = new NetworkTransmitter(this.serializer);
        this.networkMonitor = new NetworkMonitor();

        this.inboundMessages = [];
        this.outboundMessages = [];

        // create the renderer
        this.renderer = this.gameEngine.renderer = new Renderer(gameEngine, this);

        /**
        * client's player ID, as a string.
        * @member {String}
        */
        this.playerId = NaN;

        this.configureSynchronization();

        // create a buffer of delayed inputs (fifo)
        if (inputOptions && inputOptions.delayInputCount) {
            this.delayedInputs = [];
            for (let i = 0; i < inputOptions.delayInputCount; i++)
                this.delayedInputs[i] = [];
        }
    }

    /**
     * Check if a given object is owned by the player on this client
     *
     * @param {Object} object the game object to check
     * @return {Boolean} true if the game object is owned by the player on this client
     */
    isOwnedByPlayer(object) {
        return (object.playerId == this.playerId);
    }

    configureSynchronization() {

        // the reflect syncronizer is just interpolate strategy,
        // configured to show server syncs
        let syncOptions = this.options.syncOptions;
        if (syncOptions.sync === 'reflect') {
            syncOptions.sync = 'interpolate';
            syncOptions.reflect = true;
        }
        const synchronizer = new Synchronizer(this, syncOptions);
    }

    /**
     * Makes a connection to the game server
     *
     * @return {Promise} Resolved when the connection is made to the server
     */
    connect() {
        let connectionPromise = new Promise((resolve, reject) => {
            this.socket = io(this.options.serverURL);

            this.networkMonitor.registerClient(this);

            this.socket.once('connect', () => {
                console.log('connection made');
                resolve();
            });

            this.socket.on('playerJoined', (playerData) => {
                this.playerId = playerData.playerId;
                this.messageIndex = Number(this.playerId) * 10000;
            });

            this.socket.on('worldUpdate', (worldData) => {
                this.inboundMessages.push(worldData);
            });
        });

        return connectionPromise;
    }

    /**
     * Start the client engine, setting up the game loop, rendering loop and renderer.
     *
     * @return {Promise} Resolves once the Renderer has been initialized, and the game is
     * ready to connect
     */
    start() {

        // schedule and start the game loop
        this.scheduler = new Scheduler({
            period: this.options.stepPeriod,
            tick: this.step.bind(this),
            delay: STEP_DELAY_MSEC
        });
        this.gameEngine.start();
        this.scheduler.start();

        // initialize the renderer
        // the render loop waits for next animation frame
        if (!this.renderer) alert('ERROR: game has not defined a renderer');
        let renderLoop = () => {
            this.renderer.draw();
            window.requestAnimationFrame(renderLoop);
        };

        return this.renderer.init().then(() => {
            if (typeof window !== 'undefined')
                window.requestAnimationFrame(renderLoop);
            if (this.options.autoConnect) {
                this.connect();
            }
        });
    }

    step() {
        // first update the trace state
        this.gameEngine.trace.setStep(this.gameEngine.world.stepCount + 1);

        // skip one step if requested
        if (this.skipOneStep === true) {
            this.skipOneStep = false;
            return;
        }

        this.gameEngine.emit('client__preStep');
        while (this.inboundMessages.length > 0) {
            this.handleInboundMessage(this.inboundMessages.pop());
        }

        // check for server/client step drift
        if (this.gameEngine.serverStep) {
            if (this.gameEngine.world.stepCount > this.gameEngine.serverStep + STEP_DRIFT_THRESHOLD) {
                this.gameEngine.trace.warn('step drift.  Client is ahead of server.  Delaying next step.');
                this.scheduler.delayTick();
            } else if (this.gameEngine.serverStep > this.gameEngine.world.stepCount + STEP_DRIFT_THRESHOLD) {
                this.gameEngine.trace.warn('step drift.  Client is behind server.  Hurrying next step.');
                this.scheduler.hurryTick();
            }
        }

        // perform game engine step
        this.handleOutboundInput();
        this.applyDelayedInputs();
        this.gameEngine.step();
        this.gameEngine.emit('client__postStep');

        if (this.gameEngine.trace.length && this.socket) {
            // socket might not have been initialized at this point
            this.socket.emit('trace', JSON.stringify(this.gameEngine.trace.rotate()));
        }
    }

    doInputLocal(message) {
        if (this.gameEngine.passive) {
            return;
        }

        this.gameEngine.emit('client__preInput', message.data);
        this.gameEngine.processInput(message.data, this.playerId);
        this.gameEngine.emit('client__postInput', message.data);
    }

    applyDelayedInputs() {
        if (!this.delayedInputs) {
            return;
        }
        let that = this;
        let delayed = this.delayedInputs.shift();
        if (delayed && delayed.length) {
            delayed.forEach(that.doInputLocal.bind(that));
        }
        this.delayedInputs.push([]);
    }

    /**
     * This function should be called by the client whenever a user input
     * occurs.  This function will emit the input event,
     * forward the input to the client's game engine (with a delay if
     * so configured) and will transmit the input to the server as well.
     *
     * This function can be called by the extended client engine class,
     * typically at the beginning of client-side step processing (see event client__preStep)
     *
     * @param {Object} input - string representing the input
     * @param {Object} inputOptions - options for the input
     */
    sendInput(input, inputOptions) {
        var message = {
            command: 'move',
            data: {
                messageIndex: this.messageIndex,
                step: this.gameEngine.world.stepCount,
                input: input,
                options: inputOptions
            }
        };

        this.gameEngine.trace.info(`USER INPUT[${this.messageIndex}]: ${input} ${inputOptions ? JSON.stringify(inputOptions) : '{}'}`);

        // if we delay input application on client, then queue it
        // otherwise apply it now
        if (this.delayedInputs) {
            this.delayedInputs[this.delayedInputs.length - 1].push(message);
        } else {
            this.doInputLocal(message);
        }
        this.outboundMessages.push(message);

        this.messageIndex++;
    }

    handleInboundMessage(syncData) {

        let syncEvents = this.networkTransmitter.deserializePayload(syncData).events;
        let syncHeader = syncEvents.find((e) => e.eventName === 'syncHeader');

        // emit that a snapshot has been received
        this.gameEngine.emit('client__syncReceived', {
            syncEvents: syncEvents,
            stepCount: syncHeader.stepCount
        });

        this.gameEngine.trace.info(`========== inbound world update ${syncHeader.stepCount} ==========`);

        // finally update the stepCount
        if (syncHeader.stepCount > this.gameEngine.world.stepCount) {
            this.gameEngine.world.stepCount = syncHeader.stepCount;
            this.gameEngine.trace.info(`========== world step count updated to  ${syncHeader.stepCount} ==========`);
        }
    }

    handleOutboundInput() {
        for (var x = 0; x < this.outboundMessages.length; x++) {
            this.socket.emit(this.outboundMessages[x].command, this.outboundMessages[x].data);
        }
        this.outboundMessages = [];
    }

}

module.exports = ClientEngine;
<commit_msg>separate lead-lag thresholds for sync steps and other steps<commit_after>'use strict';
var io = require('socket.io-client');
const Serializer = require('./serialize/Serializer');
const NetworkTransmitter = require('./network/NetworkTransmitter');
const NetworkMonitor = require('./network/NetworkMonitor');
const Synchronizer = require('./Synchronizer');
const Scheduler = require('./lib/Scheduler');

// externalizing these parameters as options would add confusion to game
// developers, and provide no real benefit.
const STEP_DRIFT_THRESHOLDS = {
    onServerSync: { MAX_LEAD: 3, MAX_LAG: 4 }, // max step lead/lag allowed after every server sync
    onEveryStep: { MAX_LEAD: 10, MAX_LAG: 10 } // max step lead/lag allowed at every step
};
const STEP_DRIFT_THRESHOLD__CLIENT_RESET = 20; // if we are behind this many steps, just reset the step counter
const GAME_UPS = 60; // default number of game steps per second
const STEP_DELAY_MSEC = 12; // if drift detected, delay next execution by this amount

/**
 * The client engine is the singleton which manages the client-side
 * process, starting the game engine, listening to network messages,
 * starting client steps, and handling world updates which arrive from
 * the server.
 */
class ClientEngine {

    /**
      * Create a client engine instance.
      *
      * @param {GameEngine} gameEngine - a game engine
      * @param {Object} inputOptions - options object
      * @param {Boolean} inputOptions.autoConnect - if true, the client will automatically attempt connect to server.
      * @param {Number} inputOptions.delayInputCount - if set, inputs will be delayed by this many steps before they are actually applied on the client.
      * @param {Number} inputOptions.healthCheckInterval - health check message interval (millisec).  Default is 1000.
      * @param {Number} inputOptions.healthCheckRTTSample - health check RTT calculation sample size.  Default is 10.
      * @param {Object} inputOptions.syncOptions - an object describing the synchronization method.  If not set, will be set to extrapolate, with local object bending set to 0.0 and remote object bending set to 0.6.  If the query-string parameter "sync" is defined, then that value is passed to this object's sync attribute.
      * @param {String} inputOptions.syncOptions.sync - chosen sync option, can be interpolate, extrapolate, or frameSync
      * @param {Number} inputOptions.syncOptions.localObjBending - amount of bending towards original client position, after each sync, for local objects
      * @param {Number} inputOptions.syncOptions.remoteObjBending - amount of bending towards original client position, after each sync, for remote objects
      * @param {Renderer} Renderer - the Renderer class constructor
      */
    constructor(gameEngine, inputOptions, Renderer) {

        this.options = Object.assign({
            autoConnect: true,
            healthCheckInterval: 1000,
            healthCheckRTTSample: 10,
            stepPeriod: 1000 / GAME_UPS
        }, inputOptions);

        /**
         * reference to serializer
         * @member {Serializer}
         */
        this.serializer = new Serializer();

        /**
         * reference to game engine
         * @member {GameEngine}
         */
        this.gameEngine = gameEngine;
        this.networkTransmitter = new NetworkTransmitter(this.serializer);
        this.networkMonitor = new NetworkMonitor();

        this.inboundMessages = [];
        this.outboundMessages = [];

        // create the renderer
        this.renderer = this.gameEngine.renderer = new Renderer(gameEngine, this);

        /**
        * client's player ID, as a string.
        * @member {String}
        */
        this.playerId = NaN;

        this.configureSynchronization();

        // create a buffer of delayed inputs (fifo)
        if (inputOptions && inputOptions.delayInputCount) {
            this.delayedInputs = [];
            for (let i = 0; i < inputOptions.delayInputCount; i++)
                this.delayedInputs[i] = [];
        }
    }

    /**
     * Check if a given object is owned by the player on this client
     *
     * @param {Object} object the game object to check
     * @return {Boolean} true if the game object is owned by the player on this client
     */
    isOwnedByPlayer(object) {
        return (object.playerId == this.playerId);
    }

    configureSynchronization() {

        // the reflect syncronizer is just interpolate strategy,
        // configured to show server syncs
        let syncOptions = this.options.syncOptions;
        if (syncOptions.sync === 'reflect') {
            syncOptions.sync = 'interpolate';
            syncOptions.reflect = true;
        }
        const synchronizer = new Synchronizer(this, syncOptions);
    }

    /**
     * Makes a connection to the game server
     *
     * @return {Promise} Resolved when the connection is made to the server
     */
    connect() {
        let connectionPromise = new Promise((resolve, reject) => {
            this.socket = io(this.options.serverURL);

            this.networkMonitor.registerClient(this);

            this.socket.once('connect', () => {
                console.log('connection made');
                resolve();
            });

            this.socket.on('playerJoined', (playerData) => {
                this.playerId = playerData.playerId;
                this.messageIndex = Number(this.playerId) * 10000;
            });

            this.socket.on('worldUpdate', (worldData) => {
                this.inboundMessages.push(worldData);
            });
        });

        return connectionPromise;
    }

    /**
     * Start the client engine, setting up the game loop, rendering loop and renderer.
     *
     * @return {Promise} Resolves once the Renderer has been initialized, and the game is
     * ready to connect
     */
    start() {

        // schedule and start the game loop
        this.scheduler = new Scheduler({
            period: this.options.stepPeriod,
            tick: this.step.bind(this),
            delay: STEP_DELAY_MSEC
        });
        this.gameEngine.start();
        this.scheduler.start();

        // initialize the renderer
        // the render loop waits for next animation frame
        if (!this.renderer) alert('ERROR: game has not defined a renderer');
        let renderLoop = () => {
            this.renderer.draw();
            window.requestAnimationFrame(renderLoop);
        };

        return this.renderer.init().then(() => {
            if (typeof window !== 'undefined')
                window.requestAnimationFrame(renderLoop);
            if (this.options.autoConnect) {
                this.connect();
            }
        });
    }

    // check if client step is too far ahead (leading) or too far
    // behing (lagging) the server step
    checkDrift(checkType) {

        if (!this.gameEngine.serverStep)
            return;

        let maxLead = STEP_DRIFT_THRESHOLDS[checkType].MAX_LEAD;
        let maxLag = STEP_DRIFT_THRESHOLDS[checkType].MAX_LAG;
        let clientStep = this.gameEngine.world.stepCount;
        let serverStep = this.gameEngine.serverStep;
        if (clientStep > serverStep + maxLead) {
            this.gameEngine.trace.warn(`step drift ${checkType}. [${clientStep} > ${serverStep} + ${maxLead}] Client is ahead of server.  Delaying next step.`);
            this.scheduler.delayTick();
        } else if (serverStep > clientStep + maxLag) {
            this.gameEngine.trace.warn(`step drift ${checkType}. [${serverStep} > ${clientStep} + ${maxLag}] Client is behind server.  Hurrying next step.`);
            this.scheduler.hurryTick();
        }
    }

    step() {

        // first update the trace state
        this.gameEngine.trace.setStep(this.gameEngine.world.stepCount + 1);

        // skip one step if requested
        if (this.skipOneStep === true) {
            this.skipOneStep = false;
            return;
        }

        this.gameEngine.emit('client__preStep');
        while (this.inboundMessages.length > 0) {
            this.handleInboundMessage(this.inboundMessages.pop());
            this.checkDrift('onServerSync');
        }

        // check for server/client step drift without update
        this.checkDrift('onEveryStep');

        // perform game engine step
        this.handleOutboundInput();
        this.applyDelayedInputs();
        this.gameEngine.step();
        this.gameEngine.emit('client__postStep');

        if (this.gameEngine.trace.length && this.socket) {
            // socket might not have been initialized at this point
            this.socket.emit('trace', JSON.stringify(this.gameEngine.trace.rotate()));
        }
    }

    doInputLocal(message) {
        if (this.gameEngine.passive) {
            return;
        }

        this.gameEngine.emit('client__preInput', message.data);
        this.gameEngine.processInput(message.data, this.playerId);
        this.gameEngine.emit('client__postInput', message.data);
    }

    applyDelayedInputs() {
        if (!this.delayedInputs) {
            return;
        }
        let that = this;
        let delayed = this.delayedInputs.shift();
        if (delayed && delayed.length) {
            delayed.forEach(that.doInputLocal.bind(that));
        }
        this.delayedInputs.push([]);
    }

    /**
     * This function should be called by the client whenever a user input
     * occurs.  This function will emit the input event,
     * forward the input to the client's game engine (with a delay if
     * so configured) and will transmit the input to the server as well.
     *
     * This function can be called by the extended client engine class,
     * typically at the beginning of client-side step processing (see event client__preStep)
     *
     * @param {Object} input - string representing the input
     * @param {Object} inputOptions - options for the input
     */
    sendInput(input, inputOptions) {
        var message = {
            command: 'move',
            data: {
                messageIndex: this.messageIndex,
                step: this.gameEngine.world.stepCount,
                input: input,
                options: inputOptions
            }
        };

        this.gameEngine.trace.info(`USER INPUT[${this.messageIndex}]: ${input} ${inputOptions ? JSON.stringify(inputOptions) : '{}'}`);

        // if we delay input application on client, then queue it
        // otherwise apply it now
        if (this.delayedInputs) {
            this.delayedInputs[this.delayedInputs.length - 1].push(message);
        } else {
            this.doInputLocal(message);
        }
        this.outboundMessages.push(message);

        this.messageIndex++;
    }

    handleInboundMessage(syncData) {

        let syncEvents = this.networkTransmitter.deserializePayload(syncData).events;
        let syncHeader = syncEvents.find((e) => e.eventName === 'syncHeader');

        // emit that a snapshot has been received
        this.gameEngine.emit('client__syncReceived', {
            syncEvents: syncEvents,
            stepCount: syncHeader.stepCount
        });

        this.gameEngine.trace.info(`========== inbound world update ${syncHeader.stepCount} ==========`);

        // finally update the stepCount
        if (syncHeader.stepCount > this.gameEngine.world.stepCount + STEP_DRIFT_THRESHOLD__CLIENT_RESET) {
            this.gameEngine.trace.info(`========== world step count updated from ${this.gameEngine.world.stepCount} to  ${syncHeader.stepCount} ==========`);
            this.gameEngine.world.stepCount = syncHeader.stepCount;
        }
    }

    handleOutboundInput() {
        for (var x = 0; x < this.outboundMessages.length; x++) {
            this.socket.emit(this.outboundMessages[x].command, this.outboundMessages[x].data);
        }
        this.outboundMessages = [];
    }

}

module.exports = ClientEngine;
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var async = require('async');
var ObjectID = require('mongodb').ObjectID;
var rawbg = require('../lib/plugins/rawbg')();

var ONE_DAY = 86400000
  , TWO_DAYS = 172800000;

function uniq(a) {
  var seen = {};
  return a.filter(function (item) {
    return seen.hasOwnProperty(item.mills) ? false : (seen[item.mills] = true);
  });
}

function init(env, ctx) {

  var data = {
    sgvs: []
    , treatments: []
    , mbgs: []
    , cals: []
    , profiles: []
    , devicestatus: []
    , lastUpdated: 0
  };

  data.clone = function clone() {
    return _.cloneDeep(data, function (value) {
      //special handling of mongo ObjectID's
      //see https://github.com/lodash/lodash/issues/602#issuecomment-47414964
      if (value instanceof ObjectID) {
        return value.toString();
      }
    });
  };

  data.update = function update(done) {

    console.log('running data.update');
    data.lastUpdated = Date.now();

    function loadComplete (err, result) {
      data.treatments = _.uniq(data.treatments, false, function (item) { return item._id.toString(); });
      //sort treatments so the last is the most recent
      data.treatments = _.sortBy(data.treatments, function (item) { return item.mills; });
      data.updateTreatmentDisplayBGs();
      if (err) {
        console.error(err);
      }
      done(err, result);
    }

    // clear treatments, we're going to merge from more queries
    data.treatments = [];

    async.parallel([
      loadEntries.bind(null, data, ctx)
      , loadTreatments.bind(null, data, ctx)
      , loadProfileSwitchTreatments.bind(null, data, ctx)
      , loadSensorTreatments.bind(null, data, ctx)
      , loadProfile.bind(null, data, ctx)
      , loadDeviceStatus.bind(null, data, env, ctx)
    ], loadComplete);

  };

  data.calculateDelta = function calculateDelta(lastData) {
    return data.calculateDeltaBetweenDatasets(lastData,data);
  };

  data.calculateDeltaBetweenDatasets = function calculateDeltaBetweenDatasets(oldData,newData) {

    var delta = {'delta': true};
    var changesFound = false;

    // if there's no updates done so far, just return the full set
    if (!oldData.sgvs) { return newData; }

    function nsArrayTreatments(oldArray, newArray) {
      var result = [];

      // check for add, change
      var l = newArray.length;
      var m = oldArray.length;
      for (var i = 0; i < l; i++) {
        var no = newArray[i];
        var found = false;
        var founddiff = false;
        for (var j = 0; j < m; j++) {
          var oo = oldArray[j];
          no._id = no._id.toString();
          if (no._id === oo._id) {
            found = true;
            if (!_.isEqual(oo, no)) {
              founddiff = true;
            }
            break;
          }
        }
        if (founddiff) {
          var nno = _.cloneDeep(no);
          nno.action = 'update';
          result.push(nno);
        }
        if (!found) {
          result.push(no);
        }
      }

      //check for delete
      for (j = 0; j < m; j++) {
        oo = oldArray[j];
        found = false;
        for (i = 0; i < l; i++) {
          no = newArray[i];
          if (no._id === oo._id) {
            found = true;
            break;
          }
        }
        if (!found) {
          result.push({ _id: oo._id, action: 'remove' });
        }
      }

      return result;
    }

    function nsArrayDiff(oldArray, newArray) {
      var seen = {};
      var l = oldArray.length;
      for (var i = 0; i < l; i++) {
        seen[oldArray[i].mills] = true;
      }
      var result = [];
      l = newArray.length;
      for (var j = 0; j < l; j++) {
        if (!seen.hasOwnProperty(newArray[j].mills)) {
          result.push(newArray[j]);
        }
      }
      return result;
    }

    function sort(values) {
      values.sort(function sorter(a, b) {
        return a.mills - b.mills;
      });
    }

    function compressArrays(delta, newData) {
      // array compression
      var compressibleArrays = ['sgvs', 'treatments', 'mbgs', 'cals', 'devicestatus'];
      var changesFound = false;

      for (var array in compressibleArrays) {
        if (compressibleArrays.hasOwnProperty(array)) {
          var a = compressibleArrays[array];
          if (newData.hasOwnProperty(a)) {

            // if previous data doesn't have the property (first time delta?), just assign data over
            if (!oldData.hasOwnProperty(a)) {
              delta[a] = newData[a];
              changesFound = true;
              continue;
            }

            // Calculate delta and assign delta over if changes were found
            var deltaData = (a === 'treatments' ? nsArrayTreatments(oldData[a], newData[a]) : nsArrayDiff(oldData[a], newData[a]));
            if (deltaData.length > 0) {
              console.log('delta changes found on', a);
              changesFound = true;
              sort(deltaData);
              delta[a] = deltaData;
            }
          }
        }
      }
      return {'delta': delta, 'changesFound': changesFound};
    }

    function deleteSkippables(delta,newData) {
      // objects
      var skippableObjects = ['profiles'];
      var changesFound = false;

      for (var object in skippableObjects) {
        if (skippableObjects.hasOwnProperty(object)) {
          var o = skippableObjects[object];
          if (newData.hasOwnProperty(o)) {
            if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) {
              console.log('delta changes found on', o);
              changesFound = true;
              delta[o] = newData[o];
            }
          }
        }
      }
      return {'delta': delta, 'changesFound': changesFound};
    }

    delta.lastUpdated = newData.lastUpdated;

    var compressedDelta = compressArrays(delta, newData);
    delta = compressedDelta.delta;
    if (compressedDelta.changesFound) { changesFound = true; }

    var skippedDelta = deleteSkippables(delta, newData);
    delta = skippedDelta.delta;
    if (skippedDelta.changesFound) { changesFound = true; }

    if (changesFound) { return delta; }
    return newData;

  };

  data.updateTreatmentDisplayBGs = function updateTreatmentDisplayBGs () {
    function updateTreatmentBG(treatment) {

      function mgdlByTime() {

        var withBGs = _.filter(data.sgvs, function(d) {
          return d.mgdl > 39 || env.settings.isEnabled('rawbg');
        });

        var beforeTreatment = _.findLast(withBGs, function (d) {
          return d.mills <= treatment.mills;
        });
        var afterTreatment = _.find(withBGs, function (d) {
          return d.mills >= treatment.mills;
        });

        var mgdlBefore = mgdlValue(beforeTreatment) || calcRaw(beforeTreatment);
        var mgdlAfter = mgdlValue(afterTreatment) || calcRaw(afterTreatment);

        var calcedBG = 0;
        if (mgdlBefore && mgdlAfter) {
          calcedBG = (mgdlBefore + mgdlAfter) / 2;
        } else if (mgdlBefore) {
          calcedBG = mgdlBefore;
        } else if (mgdlAfter) {
          calcedBG = mgdlAfter;
        }

        return calcedBG || 400;
      }

      function mgdlValue (entry) {
        return entry && entry.mgdl >= 39 && Number(entry.mgdl);
      }

      function calcRaw (entry) {
        var raw;
        if (entry && env.settings.isEnabled('rawbg')) {
          var cal = _.last(data.cals);
          if (cal) {
            raw = rawbg.calc(entry, cal);
          }
        }
        return raw;
      }

      if (treatment.glucose && isNaN(treatment.glucose)) {
        console.warn('found an invalid glucose value', treatment);
      } else if (treatment.glucose && treatment.units) {
        if (treatment.units === 'mmol') {
          treatment.mmol = Number(treatment.glucose);
        } else {
          treatment.mgdl = Number(treatment.glucose);
        }
      } else if (treatment.glucose) {
        //no units, assume everything is the same
        console.warn('found a glucose value without any units, maybe from an old version?', _.pick(treatment, '_id', 'created_at', 'enteredBy'));
        var units = env.DISPLAY_UNITS === 'mmol' ? 'mmol' : 'mgdl';
        treatment[units] = Number(treatment.glucose);
      } else {
        treatment.mgdl = mgdlByTime();
      }
    }

    _.each(data.treatments, updateTreatmentBG);

  };

  return data;

}

function loadEntries (data, ctx, callback) {
  var q = {
    find: {
      date: {
        $gte: data.lastUpdated - TWO_DAYS
      }
    }
    , sort: {date: 1}
  };

  ctx.entries.list(q, function (err, results) {
    if (!err && results) {
      var mbgs = [];
      var sgvs = [];
      var cals = [];
      results.forEach(function (element) {
        if (element) {
          if (element.mbg) {
            mbgs.push({
              mgdl: Number(element.mbg), mills: element.date, device: element.device
            });
          } else if (element.sgv) {
            sgvs.push({
              mgdl: Number(element.sgv), mills: element.date, device: element.device, direction: element.direction, filtered: element.filtered, unfiltered: element.unfiltered, noise: element.noise, rssi: element.rssi
            });
          } else if (element.type === 'cal') {
            cals.push({
              mills: element.date, scale: element.scale, intercept: element.intercept, slope: element.slope
            });
          }
        }
      });
      data.mbgs = uniq(mbgs);
      data.sgvs = uniq(sgvs);
      data.cals = uniq(cals);
    }
    callback();
  });
}

function mergeToTreatments (data, results) {
  var treatments = results.map(function (treatment) {
    treatment.mills = new Date(treatment.created_at).getTime();

    return treatment;
  });
  data.treatments = _.union(data.treatments, treatments);
}

function loadTreatments (data, ctx, callback) {
  var tq = {
    find: {
      created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 8)).toISOString()
      }
    }
    , sort: {created_at: 1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadProfileSwitchTreatments (data, ctx, callback) {
  var tq = {
    find: {
      eventType: {
        $eq: 'Profile Switch'
      }
      , created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 31 * 12)).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadSensorTreatments (data, ctx, callback) {
  var tq = {
    find: {
      eventType: {
        $in: [ 'Sensor Start', 'Sensor Change']
      }
      , created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 32)).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadProfile (data, ctx, callback) {
  ctx.profile.last(function (err, results) {
    if (!err && results) {
      var profiles = [];
      results.forEach(function (element) {
        if (element) {
            profiles[0] = element;
        }
      });
      data.profiles = profiles;
    }
    callback();
  });
}

function loadDeviceStatus (data, env, ctx, callback) {
  var opts = {
    find: {
      created_at: {
        $gte: new Date(data.lastUpdated - TWO_DAYS).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  if (env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.advanced) {
    //not adding count: 1 restriction
  } else {
    opts.count = 1;
  }

  ctx.devicestatus.list(opts, function (err, results) {
    if (!err && results) {
      data.devicestatus = _.map(results, function eachStatus (result) {
        result.mills = new Date(result.created_at).getTime();
        if ('uploaderBattery' in result) {
          result.uploader = {
            battery: result.uploaderBattery
          };
          delete result.uploaderBattery;
        }
        return result;
      }).reverse();
    } else {
      data.devicestatus = [];
    }
    callback();
  });
}

module.exports = init;<commit_msg>codacy<commit_after>'use strict';

var _ = require('lodash');
var async = require('async');
var ObjectID = require('mongodb').ObjectID;
var rawbg = require('../lib/plugins/rawbg')();

var ONE_DAY = 86400000
  , TWO_DAYS = 172800000;

function uniq(a) {
  var seen = {};
  return a.filter(function (item) {
    return seen.hasOwnProperty(item.mills) ? false : (seen[item.mills] = true);
  });
}

function init(env, ctx) {

  var data = {
    sgvs: []
    , treatments: []
    , mbgs: []
    , cals: []
    , profiles: []
    , devicestatus: []
    , lastUpdated: 0
  };

  data.clone = function clone() {
    return _.cloneDeep(data, function (value) {
      //special handling of mongo ObjectID's
      //see https://github.com/lodash/lodash/issues/602#issuecomment-47414964
      if (value instanceof ObjectID) {
        return value.toString();
      }
    });
  };

  data.update = function update(done) {

    console.log('running data.update');
    data.lastUpdated = Date.now();

    function loadComplete (err, result) {
      data.treatments = _.uniq(data.treatments, false, function (item) { return item._id.toString(); });
      //sort treatments so the last is the most recent
      data.treatments = _.sortBy(data.treatments, function (item) { return item.mills; });
      data.updateTreatmentDisplayBGs();
      if (err) {
        console.error(err);
      }
      done(err, result);
    }

    // clear treatments, we're going to merge from more queries
    data.treatments = [];

    async.parallel([
      loadEntries.bind(null, data, ctx)
      , loadTreatments.bind(null, data, ctx)
      , loadProfileSwitchTreatments.bind(null, data, ctx)
      , loadSensorTreatments.bind(null, data, ctx)
      , loadProfile.bind(null, data, ctx)
      , loadDeviceStatus.bind(null, data, env, ctx)
    ], loadComplete);

  };

  data.calculateDelta = function calculateDelta(lastData) {
    return data.calculateDeltaBetweenDatasets(lastData,data);
  };

  data.calculateDeltaBetweenDatasets = function calculateDeltaBetweenDatasets(oldData,newData) {

    var delta = {'delta': true};
    var changesFound = false;

    // if there's no updates done so far, just return the full set
    if (!oldData.sgvs) { return newData; }

    function nsArrayTreatments(oldArray, newArray) {
      var result = [];

      // check for add, change
      var l = newArray.length;
      var m = oldArray.length;
      var found, founddiff, no, oo, i, j;
      for (i = 0; i < l; i++) {
        no = newArray[i];
        found = false;
        founddiff = false;
        for (j = 0; j < m; j++) {
          oo = oldArray[j];
          no._id = no._id.toString();
          if (no._id === oo._id) {
            found = true;
            if (!_.isEqual(oo, no)) {
              founddiff = true;
            }
            break;
          }
        }
        if (founddiff) {
          var nno = _.cloneDeep(no);
          nno.action = 'update';
          result.push(nno);
        }
        if (!found) {
          result.push(no);
        }
      }

      //check for delete
      for (j = 0; j < m; j++) {
        oo = oldArray[j];
        found = false;
        for (i = 0; i < l; i++) {
          no = newArray[i];
          if (no._id === oo._id) {
            found = true;
            break;
          }
        }
        if (!found) {
          result.push({ _id: oo._id, action: 'remove' });
        }
      }

      return result;
    }

    function nsArrayDiff(oldArray, newArray) {
      var seen = {};
      var l = oldArray.length;
      for (var i = 0; i < l; i++) {
        seen[oldArray[i].mills] = true;
      }
      var result = [];
      l = newArray.length;
      for (var j = 0; j < l; j++) {
        if (!seen.hasOwnProperty(newArray[j].mills)) {
          result.push(newArray[j]);
        }
      }
      return result;
    }

    function sort(values) {
      values.sort(function sorter(a, b) {
        return a.mills - b.mills;
      });
    }

    function compressArrays(delta, newData) {
      // array compression
      var compressibleArrays = ['sgvs', 'treatments', 'mbgs', 'cals', 'devicestatus'];
      var changesFound = false;

      for (var array in compressibleArrays) {
        if (compressibleArrays.hasOwnProperty(array)) {
          var a = compressibleArrays[array];
          if (newData.hasOwnProperty(a)) {

            // if previous data doesn't have the property (first time delta?), just assign data over
            if (!oldData.hasOwnProperty(a)) {
              delta[a] = newData[a];
              changesFound = true;
              continue;
            }

            // Calculate delta and assign delta over if changes were found
            var deltaData = (a === 'treatments' ? nsArrayTreatments(oldData[a], newData[a]) : nsArrayDiff(oldData[a], newData[a]));
            if (deltaData.length > 0) {
              console.log('delta changes found on', a);
              changesFound = true;
              sort(deltaData);
              delta[a] = deltaData;
            }
          }
        }
      }
      return {'delta': delta, 'changesFound': changesFound};
    }

    function deleteSkippables(delta,newData) {
      // objects
      var skippableObjects = ['profiles'];
      var changesFound = false;

      for (var object in skippableObjects) {
        if (skippableObjects.hasOwnProperty(object)) {
          var o = skippableObjects[object];
          if (newData.hasOwnProperty(o)) {
            if (JSON.stringify(newData[o]) !== JSON.stringify(oldData[o])) {
              console.log('delta changes found on', o);
              changesFound = true;
              delta[o] = newData[o];
            }
          }
        }
      }
      return {'delta': delta, 'changesFound': changesFound};
    }

    delta.lastUpdated = newData.lastUpdated;

    var compressedDelta = compressArrays(delta, newData);
    delta = compressedDelta.delta;
    if (compressedDelta.changesFound) { changesFound = true; }

    var skippedDelta = deleteSkippables(delta, newData);
    delta = skippedDelta.delta;
    if (skippedDelta.changesFound) { changesFound = true; }

    if (changesFound) { return delta; }
    return newData;

  };

  data.updateTreatmentDisplayBGs = function updateTreatmentDisplayBGs () {
    function updateTreatmentBG(treatment) {

      function mgdlByTime() {

        var withBGs = _.filter(data.sgvs, function(d) {
          return d.mgdl > 39 || env.settings.isEnabled('rawbg');
        });

        var beforeTreatment = _.findLast(withBGs, function (d) {
          return d.mills <= treatment.mills;
        });
        var afterTreatment = _.find(withBGs, function (d) {
          return d.mills >= treatment.mills;
        });

        var mgdlBefore = mgdlValue(beforeTreatment) || calcRaw(beforeTreatment);
        var mgdlAfter = mgdlValue(afterTreatment) || calcRaw(afterTreatment);

        var calcedBG = 0;
        if (mgdlBefore && mgdlAfter) {
          calcedBG = (mgdlBefore + mgdlAfter) / 2;
        } else if (mgdlBefore) {
          calcedBG = mgdlBefore;
        } else if (mgdlAfter) {
          calcedBG = mgdlAfter;
        }

        return calcedBG || 400;
      }

      function mgdlValue (entry) {
        return entry && entry.mgdl >= 39 && Number(entry.mgdl);
      }

      function calcRaw (entry) {
        var raw;
        if (entry && env.settings.isEnabled('rawbg')) {
          var cal = _.last(data.cals);
          if (cal) {
            raw = rawbg.calc(entry, cal);
          }
        }
        return raw;
      }

      if (treatment.glucose && isNaN(treatment.glucose)) {
        console.warn('found an invalid glucose value', treatment);
      } else if (treatment.glucose && treatment.units) {
        if (treatment.units === 'mmol') {
          treatment.mmol = Number(treatment.glucose);
        } else {
          treatment.mgdl = Number(treatment.glucose);
        }
      } else if (treatment.glucose) {
        //no units, assume everything is the same
        console.warn('found a glucose value without any units, maybe from an old version?', _.pick(treatment, '_id', 'created_at', 'enteredBy'));
        var units = env.DISPLAY_UNITS === 'mmol' ? 'mmol' : 'mgdl';
        treatment[units] = Number(treatment.glucose);
      } else {
        treatment.mgdl = mgdlByTime();
      }
    }

    _.each(data.treatments, updateTreatmentBG);

  };

  return data;

}

function loadEntries (data, ctx, callback) {
  var q = {
    find: {
      date: {
        $gte: data.lastUpdated - TWO_DAYS
      }
    }
    , sort: {date: 1}
  };

  ctx.entries.list(q, function (err, results) {
    if (!err && results) {
      var mbgs = [];
      var sgvs = [];
      var cals = [];
      results.forEach(function (element) {
        if (element) {
          if (element.mbg) {
            mbgs.push({
              mgdl: Number(element.mbg), mills: element.date, device: element.device
            });
          } else if (element.sgv) {
            sgvs.push({
              mgdl: Number(element.sgv), mills: element.date, device: element.device, direction: element.direction, filtered: element.filtered, unfiltered: element.unfiltered, noise: element.noise, rssi: element.rssi
            });
          } else if (element.type === 'cal') {
            cals.push({
              mills: element.date, scale: element.scale, intercept: element.intercept, slope: element.slope
            });
          }
        }
      });
      data.mbgs = uniq(mbgs);
      data.sgvs = uniq(sgvs);
      data.cals = uniq(cals);
    }
    callback();
  });
}

function mergeToTreatments (data, results) {
  var treatments = results.map(function (treatment) {
    treatment.mills = new Date(treatment.created_at).getTime();

    return treatment;
  });
  data.treatments = _.union(data.treatments, treatments);
}

function loadTreatments (data, ctx, callback) {
  var tq = {
    find: {
      created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 8)).toISOString()
      }
    }
    , sort: {created_at: 1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadProfileSwitchTreatments (data, ctx, callback) {
  var tq = {
    find: {
      eventType: {
        $eq: 'Profile Switch'
      }
      , created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 31 * 12)).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadSensorTreatments (data, ctx, callback) {
  var tq = {
    find: {
      eventType: {
        $in: [ 'Sensor Start', 'Sensor Change']
      }
      , created_at: {
        $gte: new Date(data.lastUpdated - (ONE_DAY * 32)).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  ctx.treatments.list(tq, function (err, results) {
    if (!err && results) {
      mergeToTreatments(data, results);
    }

    callback();
  });
}

function loadProfile (data, ctx, callback) {
  ctx.profile.last(function (err, results) {
    if (!err && results) {
      var profiles = [];
      results.forEach(function (element) {
        if (element) {
            profiles[0] = element;
        }
      });
      data.profiles = profiles;
    }
    callback();
  });
}

function loadDeviceStatus (data, env, ctx, callback) {
  var opts = {
    find: {
      created_at: {
        $gte: new Date(data.lastUpdated - TWO_DAYS).toISOString()
      }
    }
    , sort: {created_at: -1}
  };

  if (env.extendedSettings.devicestatus && env.extendedSettings.devicestatus.advanced) {
    //not adding count: 1 restriction
  } else {
    opts.count = 1;
  }

  ctx.devicestatus.list(opts, function (err, results) {
    if (!err && results) {
      data.devicestatus = _.map(results, function eachStatus (result) {
        result.mills = new Date(result.created_at).getTime();
        if ('uploaderBattery' in result) {
          result.uploader = {
            battery: result.uploaderBattery
          };
          delete result.uploaderBattery;
        }
        return result;
      }).reverse();
    } else {
      data.devicestatus = [];
    }
    callback();
  });
}

module.exports = init;<|endoftext|>


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

// User service
flowableApp.service('RuntimeAppDefinitionService', ['$http', '$q', '$location', 'appName',
    function ($http, $q, $location, appName) {

        var httpAsPromise = function(options) {
            var deferred = $q.defer();
            $http(options).
                success(function (response, status, headers, config) {
                    deferred.resolve(response);
                })
                .error(function (response, status, headers, config) {
                    deferred.reject(response);
                });
            return deferred.promise;
        };

        this.getApplications = function () {

            var defaultApps = [];

            // Determine the full url with a context root (if any)
            var baseUrl = $location.absUrl();
            var index = baseUrl.indexOf('/#');
            if (index >= 0) {
                baseUrl = baseUrl.substring(0, index);
            }
            index = baseUrl.indexOf('?');
            if (index >= 0) {
                baseUrl = baseUrl.substring(0, index);
            }
            if (baseUrl[baseUrl.length - 1] == '/') {
                baseUrl = baseUrl.substring(0, baseUrl.length - 1);
            }
            if (appName.length > 0 && baseUrl.substring(baseUrl.length - appName.length) == appName) {
                baseUrl = baseUrl.substring(0, baseUrl.length - appName.length - 1);
            }

            var transformAppsResponse = function(value, headersGetter, status) {
                if (status !== 200) {
                    return;
                }

                var response = JSON.parse(value);
                var customApps = [];
                for (var i = 0; i < response.data.length; i++) {

                    var app = response.data[i];
                    if (app.defaultAppId !== undefined && app.defaultAppId !== null) {

                        if (app.defaultAppId === 'tasks') {

                            defaultApps.push(
                                {
                                    id: 'tasks',
                                    titleKey: 'APP.TASKS.TITLE',
                                    descriptionKey: 'APP.TASKS.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-2',
                                    icon: 'icon icon-clock',
                                    fixedBaseUrl: baseUrl + '/workflow/' + '/#/',
                                    fixedUrl: baseUrl + '/workflow/',
                                    pages: ['tasks', 'processes']
                                });
                        } else if (app.defaultAppId === 'admin') {

                            defaultApps.push(
                                {
                                    id: 'admin',
                                    titleKey: 'APP.ADMIN.TITLE',
                                    descriptionKey: 'APP.ADMIN.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-2',
                                    icon: 'icon icon-clock',
                                    fixedBaseUrl: baseUrl + '/admin/' + '/#/',
                                    fixedUrl: baseUrl + '/admin/',
                                    pages: []
                                });
                        } else if (app.defaultAppId === 'idm') {

                            defaultApps.push(
                                {
                                    id: 'idm',
                                    titleKey: 'APP.IDM.TITLE',
                                    descriptionKey: 'APP.IDM.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-2',
                                    icon: 'icon icon-clock',
                                    fixedBaseUrl: baseUrl + '/idm/' + '/#/',
                                    fixedUrl: baseUrl + '/idm/',
                                    pages: []
                                });
                        } else if (app.defaultAppId === 'modeler') {

                            defaultApps.push(
                                {
                                    id: 'modeler',
                                    titleKey: 'APP.MODELER.TITLE',
                                    descriptionKey: 'APP.MODELER.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-2',
                                    icon: 'icon icon-clock',
                                    fixedBaseUrl: baseUrl + '/modeler/' + '/#/',
                                    fixedUrl: baseUrl + '/modeler/',
                                    pages: []
                                });
                        }

                    } else {

                        // Custom app
                        app.icon = 'glyphicon ' + app.icon;
                        app.fixedBaseUrl = baseUrl + '/workflow/#/apps/' + app.appDefinitionKey + '/';
                        app.fixedUrl = app.fixedBaseUrl + 'tasks';
                        app.pages = [ 'tasks', 'processes' ];
                        app.deletable = true;
                        customApps.push(app);
                    }

                }

                return {
                    defaultApps: defaultApps,
                    customApps: customApps
                };
            };

            return httpAsPromise({
                method: 'GET',
                url: FLOWABLE.CONFIG.contextRoot + '/app/rest/runtime/app-definitions',
                transformResponse: transformAppsResponse
            });
        };

        this.deleteAppDefinition = function (appDefinitionKey) {
            var promise = httpAsPromise({
                method: 'DELETE',
                url: FLOWABLE.CONFIG.contextRoot + '/app/rest/runtime/app-definitions/' + appDefinitionKey
            });

            return promise;
        };
    }]);<commit_msg>Different icons for different UI applications<commit_after>/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

// User service
flowableApp.service('RuntimeAppDefinitionService', ['$http', '$q', '$location', 'appName',
    function ($http, $q, $location, appName) {

        var httpAsPromise = function(options) {
            var deferred = $q.defer();
            $http(options).
                success(function (response, status, headers, config) {
                    deferred.resolve(response);
                })
                .error(function (response, status, headers, config) {
                    deferred.reject(response);
                });
            return deferred.promise;
        };

        this.getApplications = function () {

            var defaultApps = [];

            // Determine the full url with a context root (if any)
            var baseUrl = $location.absUrl();
            var index = baseUrl.indexOf('/#');
            if (index >= 0) {
                baseUrl = baseUrl.substring(0, index);
            }
            index = baseUrl.indexOf('?');
            if (index >= 0) {
                baseUrl = baseUrl.substring(0, index);
            }
            if (baseUrl[baseUrl.length - 1] == '/') {
                baseUrl = baseUrl.substring(0, baseUrl.length - 1);
            }
            if (appName.length > 0 && baseUrl.substring(baseUrl.length - appName.length) == appName) {
                baseUrl = baseUrl.substring(0, baseUrl.length - appName.length - 1);
            }

            var transformAppsResponse = function(value, headersGetter, status) {
                if (status !== 200) {
                    return;
                }

                var response = JSON.parse(value);
                var customApps = [];
                for (var i = 0; i < response.data.length; i++) {

                    var app = response.data[i];
                    if (app.defaultAppId !== undefined && app.defaultAppId !== null) {

                        if (app.defaultAppId === 'tasks') {

                            defaultApps.push(
                                {
                                    id: 'tasks',
                                    titleKey: 'APP.TASKS.TITLE',
                                    descriptionKey: 'APP.TASKS.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-8',
                                    icon: 'glyphicon glyphicon-list-alt',
                                    fixedBaseUrl: baseUrl + '/workflow/' + '/#/',
                                    fixedUrl: baseUrl + '/workflow/',
                                    pages: ['tasks', 'processes']
                                });
                        } else if (app.defaultAppId === 'admin') {

                            defaultApps.push(
                                {
                                    id: 'admin',
                                    titleKey: 'APP.ADMIN.TITLE',
                                    descriptionKey: 'APP.ADMIN.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-2',
                                    icon: 'glyphicon glyphicon-wrench',
                                    fixedBaseUrl: baseUrl + '/admin/' + '/#/',
                                    fixedUrl: baseUrl + '/admin/',
                                    pages: []
                                });
                        } else if (app.defaultAppId === 'idm') {

                            defaultApps.push(
                                {
                                    id: 'idm',
                                    titleKey: 'APP.IDM.TITLE',
                                    descriptionKey: 'APP.IDM.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-3',
                                    icon: 'icon icon-user',
                                    fixedBaseUrl: baseUrl + '/idm/' + '/#/',
                                    fixedUrl: baseUrl + '/idm/',
                                    pages: []
                                });
                        } else if (app.defaultAppId === 'modeler') {

                            defaultApps.push(
                                {
                                    id: 'modeler',
                                    titleKey: 'APP.MODELER.TITLE',
                                    descriptionKey: 'APP.MODELER.DESCRIPTION',
                                    defaultAppId : app.defaultAppId,
                                    theme: 'theme-5',
                                    icon: 'icon icon-bpmn-stencil',
                                    fixedBaseUrl: baseUrl + '/modeler/' + '/#/',
                                    fixedUrl: baseUrl + '/modeler/',
                                    pages: []
                                });
                        }

                    } else {

                        // Custom app
                        app.icon = 'glyphicon ' + app.icon;
                        app.fixedBaseUrl = baseUrl + '/workflow/#/apps/' + app.appDefinitionKey + '/';
                        app.fixedUrl = app.fixedBaseUrl + 'tasks';
                        app.pages = [ 'tasks', 'processes' ];
                        app.deletable = true;
                        customApps.push(app);
                    }

                }

                return {
                    defaultApps: defaultApps,
                    customApps: customApps
                };
            };

            return httpAsPromise({
                method: 'GET',
                url: FLOWABLE.CONFIG.contextRoot + '/app/rest/runtime/app-definitions',
                transformResponse: transformAppsResponse
            });
        };

        this.deleteAppDefinition = function (appDefinitionKey) {
            var promise = httpAsPromise({
                method: 'DELETE',
                url: FLOWABLE.CONFIG.contextRoot + '/app/rest/runtime/app-definitions/' + appDefinitionKey
            });

            return promise;
        };
    }]);<|endoftext|>


<commit_before>let React = require("react")
let cx = require("classnames")
let popup = React.createFactory(require("./timepicker_popup"))
let {errorList, sizeClassNames, formGroupCx, label} = require("../util.js")
let {div, input} = React.DOM

export default class extends React.Component {

  displayName = "Frig.friggingBootstrap.TimePicker"

  static defaultProps = Object.assign(require("../default_props.js"))

  state = {
    showPopup: false,
  }

  _onInputClick() {
    if (this.state.showPopup === true) return undefined
    this.setState({ showPopup: true })
  }

  _onPopupBGClick() {
    this.setState({ showPopup: false })
  }

  _input() {
    return input(Object.assign({}, this.props.inputHtml, {
        valueLink: this.props.valueLink,
        className: cx(
          this.props.inputHtml.className,
          "frig-timepicker-input",
          "form-control",
        ),
        onClick: this._onInputClick.bind(this),
      })
    )
  }

  _inputPopup() {
    if(this.state.showPopup === false) return

    return [
      div({
        className: "frigb-popup-bg",
        onClick: this._onPopupBGClick.bind(this),
      }),
      popup({ valueLink: this.props.valueLink }),
    ]
  }

  render() {
    return div({className: cx(sizeClassNames(this.props))},
      div({className: formGroupCx(this.props)},
        div({},
          label(this.props)
        ),
        this._input(),
        errorList(this.props.errors),
      ),
      this._inputPopup(),
    )
  }

}
<commit_msg>Turn Off Time Picker When Tab Is Press<commit_after>let React = require("react")
let cx = require("classnames")
let popup = React.createFactory(require("./timepicker_popup"))
let {errorList, sizeClassNames, formGroupCx, label} = require("../util.js")
let {div, input} = React.DOM

export default class extends React.Component {

  displayName = "Frig.friggingBootstrap.TimePicker"

  static defaultProps = Object.assign(require("../default_props.js"))

  state = {
    showPopup: false,
  }

  _onKeydown(e) {
    if (e.key === "Tab") this._turnOffColorPopup()
  }

  _turnOnTimePopup() {
    if (this.state.showPopup === true) return undefined
    this.setState({ showPopup: true })
  }

  _turnOffColorPopup() {
    this.setState({ showPopup: false })
  }

  _input() {
    return input(Object.assign({}, this.props.inputHtml, {
        valueLink: this.props.valueLink,
        className: cx(
          this.props.inputHtml.className,
          "frig-timepicker-input",
          "form-control",
        ),
        onClick: this._turnOnTimePopup.bind(this),
        onKeyDownCapture: this._onKeydown.bind(this),
      })
    )
  }

  _timePopup() {
    if(this.state.showPopup === false) return

    return [
      div({
        className: "frigb-popup-bg",
        onClick: this._turnOffColorPopup.bind(this),
      }),
      popup({ valueLink: this.props.valueLink }),
    ]
  }

  render() {
    return div({className: cx(sizeClassNames(this.props))},
      div({className: formGroupCx(this.props)},
        div({}, label(this.props)),
        this._input(),
        errorList(this.props.errors),
      ),
      this._timePopup(),
    )
  }

}
<|endoftext|>


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

const fs = require('fs');

/**
 * Database & IO handler class.
 */
module.exports = class Data {

    /**
     * Initialize the class.
     * After instantiating, call init().
     * @param {Util} utils - The utilities object needed for logging, etc.
     */
    constructor(utils) {
        this.DATA_DIR = './data';
        this.DATABASE_FILE = this.DATA_DIR + '/db.json';
        this.STATE_FILE = this.DATA_DIR + '/state.json';

        this.utils = utils;
    }

    /**
     * Sets up the environment for proper data actions.
     * @return {Promise} Resolves when done setting up.
     */
    createDataDir() {
        return new Promise((resolve, reject) => {
            fs.mkdir(this.DATA_DIR, (error) => {
                if (error) {
                    if (error.code !== 'EEXIST') {
                        reject(error);
                        return;
                    }
                }

                resolve();
            });
        });
    }

    /**
     * Saves new tweet data into the database.
     * @param {Object} newData - The new tweet data to be saved.
     * @return {Promise} Resolves when done saving data
     */
    saveTweetData(newData) {
        return this.readFile(this.DATABASE_FILE).then((rawData) => {
            let data;

            try {
                data = JSON.parse(rawData);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse state data');
                throw error;
            }

            this.insertData(data, newData);

            return data;
        }, (error) => {
            if (error.code === 'ENOENT') {
                // Resolve with new data if a database doesn't exist
                return newData;
            } else {
                throw error;
            }

        }).then((dataToSave) => {
            return new Promise((resolve, reject) => {
                fs.writeFile(this.DATABASE_FILE, JSON.stringify(dataToSave), (error) => {
                    if (error) {
                        this.utils.logError('I/O: Failed to write data to database file (' + this.DATABASE_FILE + ')');
                        reject(error);
                        return;
                    }

                    this.utils.log('I/O: Saved tweet data to database');
                    resolve();
                });
            });
        });
    }

    /**
     * Inserts new data to a data object. Add to the existingData by reference.
     * @private
     * @param {Object} newData - The new tweet data to be saved.
     */
    insertData(data, newData) {
        for (const key in newData) {
            if (key in data) {
                for (const entry of newData[key]) {
                    data[key].push(entry);
                }
            } else {
                data[key] = newData[key];
            }
        }
    }

    /**
     * Reads the tweet data from the database.
     * @return {Object} The tweet data object from the database.
     */
    readTweetData() {
        return this.readFile(this.DATABASE_FILE).then((data) => {
            try {
                return JSON.parse(data);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse database data');
                throw error;
            }
        }, (error) => {
            if (error.code === 'ENOENT') {
                this.utils.logError('I/O: No database file found');
            }
            throw error;
        });
    }

    /**
     * Saves the state in file.
     * @param {Object} state - The bot's state object.
     * @return {Promise} Resolves when done saving.
     */
    saveState(state) {
        return new Promise((resolve, reject) => {
            fs.writeFile(this.STATE_FILE, JSON.stringify(state), (error) => {
                if (error) {
                    this.utils.logError('I/O: Failed to save bot state to file');
                    reject(error);
                    return;
                }

                this.utils.log('I/O: Saved bot state to database');
                resolve();
            });
        });
    }

    /**
     * Gets the state from the saved state.
     * @return {Promise} Resolves with the read state object.
     */
    readState() {
        return this.readFile(this.STATE_FILE).then((data) => {
            try {
                return JSON.parse(data);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse state data');
                throw error;
            }
        }, (error) => {
            // If no state file exists..
            if (error.code === 'ENOENT') {
                return null;
            } else {
                this.utils.logError('I/O: Failed to read data from state file');
                throw error;
            }
        });
    }
    /**
     * Reads data from a file.
     * @private
     * @param {string} filePath - The path to the file.
     * @return {Promise} Resolves with data read from the file.
     */
    readFile(filePath) {
        return new Promise((resolve, reject) => {
            // Check that file exists..
            /* eslint no-unused-vars: "off" */
            fs.stat(filePath, (error, stats) => {
                if (error) {
                    reject(error);
                    return;
                }

                // .. then read
                fs.readFile(filePath, 'utf8', (error, data) => {
                    if (error) {
                        reject(error);
                        return;
                    }

                    resolve(data);
                });
            });
        });
    }

};
<commit_msg>Cleanup unnecessary comments & formatting<commit_after>/*
 * NTwitBot - data.js
 * Copyright (C) 2016-2017 Jordan Sne
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

const fs = require('fs');

/**
 * Database & IO handler class.
 */
module.exports = class Data {

    /**
     * Initialize the class.
     * @param {Util} utils - The utilities object needed for logging, etc.
     */
    constructor(utils) {
        this.DATA_DIR = './data';
        this.DATABASE_FILE = this.DATA_DIR + '/db.json';
        this.STATE_FILE = this.DATA_DIR + '/state.json';

        this.utils = utils;
    }

    /**
     * Sets up the environment for proper data actions.
     * @return {Promise} Resolves when done setting up.
     */
    createDataDir() {
        return new Promise((resolve, reject) => {
            fs.mkdir(this.DATA_DIR, (error) => {
                if (error) {
                    if (error.code !== 'EEXIST') {
                        reject(error);
                        return;
                    }
                }

                resolve();
            });
        });
    }

    /**
     * Saves new tweet data into the database.
     * @param {Object} newData - The new tweet data to be saved.
     * @return {Promise} Resolves when done saving data
     */
    saveTweetData(newData) {
        return this.readFile(this.DATABASE_FILE).then((rawData) => {
            let data;

            try {
                data = JSON.parse(rawData);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse state data');
                throw error;
            }

            this.insertData(data, newData);

            return data;
        }, (error) => {
            if (error.code === 'ENOENT') {
                return newData;
            } else {
                throw error;
            }

        }).then((dataToSave) => {
            return new Promise((resolve, reject) => {
                fs.writeFile(this.DATABASE_FILE, JSON.stringify(dataToSave), (error) => {
                    if (error) {
                        this.utils.logError('I/O: Failed to write data to database file (' + this.DATABASE_FILE + ')');
                        reject(error);
                        return;
                    }

                    this.utils.log('I/O: Saved tweet data to database');
                    resolve();
                });
            });
        });
    }

    /**
     * Inserts new data to a data object. Add to the existingData by reference.
     * @private
     * @param {Object} newData - The new tweet data to be saved.
     */
    insertData(data, newData) {
        for (const key in newData) {
            if (key in data) {
                for (const entry of newData[key]) {
                    data[key].push(entry);
                }
            } else {
                data[key] = newData[key];
            }
        }
    }

    /**
     * Reads the tweet data from the database.
     * @return {Object} The tweet data object from the database.
     */
    readTweetData() {
        return this.readFile(this.DATABASE_FILE).then((data) => {
            try {
                return JSON.parse(data);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse database data');
                throw error;
            }
        }, (error) => {
            if (error.code === 'ENOENT') {
                this.utils.logError('I/O: No database file found');
            }
            throw error;
        });
    }

    /**
     * Saves the state in file.
     * @param {Object} state - The bot's state object.
     * @return {Promise} Resolves when done saving.
     */
    saveState(state) {
        return new Promise((resolve, reject) => {
            fs.writeFile(this.STATE_FILE, JSON.stringify(state), (error) => {
                if (error) {
                    this.utils.logError('I/O: Failed to save bot state to file');
                    reject(error);
                    return;
                }

                this.utils.log('I/O: Saved bot state to database');
                resolve();
            });
        });
    }

    /**
     * Gets the state from the saved state.
     * @return {Promise} Resolves with the read state object.
     */
    readState() {
        return this.readFile(this.STATE_FILE).then((data) => {
            try {
                return JSON.parse(data);
            } catch (error) {
                this.utils.logError('I/O: Failed to parse state data');
                throw error;
            }
        }, (error) => {
            if (error.code === 'ENOENT') {
                return null;
            } else {
                this.utils.logError('I/O: Failed to read data from state file');
                throw error;
            }
        });
    }

    /**
     * Reads data from a file.
     * @private
     * @param {string} filePath - The path to the file.
     * @return {Promise} Resolves with data read from the file.
     */
    readFile(filePath) {
        return new Promise((resolve, reject) => {
            // Check that file exists first
            fs.stat(filePath, (error) => {
                if (error) {
                    reject(error);
                    return;
                }

                fs.readFile(filePath, 'utf8', (error, data) => {
                    if (error) {
                        reject(error);
                        return;
                    }

                    resolve(data);
                });
            });
        });
    }

};
<|endoftext|>


<commit_before>'use strict';

angular.module('angularOauth', []).

  provider('Token', function() {

    /**
     * Given an flat object, returns a query string for use in URLs.  Note
     * that for a given object, the return value may be.
     *
     * @example
     *

         // returns 'color=red&size=large'
         objectToQueryString({color: 'red', size: 'large'})
     *

     *
     * @param {Object} obj A flat object containing keys for such a string.
     * @returns {string} A string suitable as a query string.
     */
    var objectToQueryString = function(obj) {
      var str = [];
      angular.forEach(obj, function(value, key) {
        str.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
      });
      return str.join("&");
    };

    // This response_type MUST be passed to the authorization endpoint using
    // the implicit grant flow (4.2.1 of RFC 6749).
    var RESPONSE_TYPE = 'token';

    // Create a special object for config fields that are required and missing.
    // If any config items still contain it when Token is used, raise an error.
    var REQUIRED_AND_MISSING = {};

    var config = {
      clientId: REQUIRED_AND_MISSING,
      redirectUri: REQUIRED_AND_MISSING,
      authorizationEndpoint: REQUIRED_AND_MISSING,
      localStorageName: 'accessToken',
      verifyFunc: REQUIRED_AND_MISSING,
      scopes: []
    };

    this.extendConfig = function(configExtension) {
      config = angular.extend(config, configExtension);
    };

    this.$get = function($q, $http, $window, $rootScope) {
      var requiredAndMissing = [];
      angular.forEach(config, function(value, key) {
        if (value === REQUIRED_AND_MISSING) {
          requiredAndMissing.push(key);
        }
      });

      if (requiredAndMissing.length) {
        throw new Error("TokenProvider is insufficiently configured.  Please " +
          "configure the following options using " +
          "TokenProvider.extendConfig: " + requiredAndMissing.join(", "))
      }

      if (!config.clientId) {
        throw new Error("clientId needs to be configured using TokenProvider.");
      }

      var getParams = function() {
        // TODO: Facebook uses comma-delimited scopes. This is not compliant with section 3.3 but perhaps support later.

        return {
          response_type: RESPONSE_TYPE,
          client_id: config.clientId,
          redirect_uri: config.redirectUri,
          scope: config.scopes.join(" ")
        }
      };

      return {
        // TODO: get/set might want to support expiration to reauthenticate
        // TODO: check for localStorage support and otherwise perhaps use other methods of storing data (e.g. cookie)

        /**
         * Returns the stored access token.
         *
         * @returns {string} The access token.
         */
        get: function() {
          return localStorage[config.localStorageName];
        },

        /**
         * Persist the access token so that it can be retrieved later by.
         *
         * @param accessToken
         */
        set: function(accessToken) {
          localStorage[config.localStorageName] = accessToken;
        },

        /**
         * Verifies that the access token is was issued for the use of the current client.
         *
         * @param accessToken An access token received from the authorization server.
         * @returns {Promise} Promise that will be resolved when the authorization server has verified that the
         *  token is valid, and we've verified that the token is passed back has audience that matches our client
         *  ID (to prevent the Confused Deputy Problem).
         *
         *  If there's an error verifying the token, the promise is rejected with an object identifying the `name` error
         *  in the name member.  The `name` can be either:
         *
         *    - `invalid_audience`: The audience didn't match our client ID.
         *    - `error_response`: The server responded with an error, typically because the token was invalid.  In this
         *      case, the callback parameters to `error` callback on `$http` are available in the object (`data`,
         *      `status`, `headers`, `config`).
         */
        verifyAsync: function(accessToken) {
          return config.verifyFunc(config, accessToken);
        },

        /**
         * Verifies an access token asynchronously.
         *
         * @param extraParams An access token received from the authorization server.
         * @param popupOptions Settings for the display of the popup.
         * @returns {Promise} Promise that will be resolved when the authorization server has verified that the
         *  token is valid, and we've verified that the token is passed back has audience that matches our client
         *  ID (to prevent the Confused Deputy Problem).
         *
         *  If there's an error verifying the token, the promise is rejected with an object identifying the `name` error
         *  in the name member.  The `name` can be either:
         *
         *    - `invalid_audience`: The audience didn't match our client ID.
         *    - `error_response`: The server responded with an error, typically because the token was invalid.  In this
         *      case, the callback parameters to `error` callback on `$http` are available in the object (`data`,
         *      `status`, `headers`, `config`).
         */
        getTokenByPopup: function(extraParams, popupOptions) {
          popupOptions = angular.extend({
            name: 'AuthPopup',
            openParams: {
              width: 650,
              height: 300,
              resizable: true,
              scrollbars: true,
              status: true
            }
          }, popupOptions);

          var deferred = $q.defer(),
            params = angular.extend(getParams(), extraParams),
            url = config.authorizationEndpoint + '?' + objectToQueryString(params),
            resolved = false;

          var formatPopupOptions = function(options) {
            var pairs = [];
            angular.forEach(options, function(value, key) {
              if (value || value === 0) {
                value = value === true ? 'yes' : value;
                pairs.push(key + '=' + value);
              }
            });
            return pairs.join(',');
          };

          var popup = window.open(url, popupOptions.name, formatPopupOptions(popupOptions.openParams));

          // TODO: binding occurs for each reauthentication, leading to leaks for long-running apps.

          angular.element($window).bind('message', function(event) {
            if (event.source == popup && event.origin == window.location.origin) {
              $rootScope.$apply(function() {
                if (event.data.access_token) {
                  deferred.resolve(event.data)
                } else {
                  deferred.reject(event.data)
                }
              })
            }
          });

          // TODO: reject deferred if the popup was closed without a message being delivered + maybe offer a timeout

          return deferred.promise;
        }
      }
    }
  }).

  /**
   * A controller for the redirect endpoint that inspects the URL redirected to by the authorization server and sends
   * it back to other windows using.
   */
  controller('CallbackCtrl', function($scope, $location) {

    /**
     * Parses an escaped url query string into key-value pairs.
     *
     * (Copied from Angular.js in the AngularJS project.)
     *
     * @returns Object.<(string|boolean)>
     */
    function parseKeyValue(/**string*/keyValue) {
      var obj = {}, key_value, key;
      angular.forEach((keyValue || "").split('&'), function(keyValue){
        if (keyValue) {
          key_value = keyValue.split('=');
          key = decodeURIComponent(key_value[0]);
          obj[key] = angular.isDefined(key_value[1]) ? decodeURIComponent(key_value[1]) : true;
        }
      });
      return obj;
    }

    var queryString = $location.path().substring(1);  // preceding slash omitted
    var params = parseKeyValue(queryString);

    // TODO: The target origin should be set to an explicit origin.  Otherwise, a malicious site that can receive
    //       the token if it manages to change the location of the parent. (See:
    //       https://developer.mozilla.org/en/docs/DOM/window.postMessage#Security_concerns)

    window.opener.postMessage(params, "*");
    window.close();
  });
<commit_msg>Allow configurable response type<commit_after>'use strict';

angular.module('angularOauth', []).

  provider('Token', function() {

    /**
     * Given an flat object, returns a query string for use in URLs.  Note
     * that for a given object, the return value may be.
     *
     * @example
     *

         // returns 'color=red&size=large'
         objectToQueryString({color: 'red', size: 'large'})
     *

     *
     * @param {Object} obj A flat object containing keys for such a string.
     * @returns {string} A string suitable as a query string.
     */
    var objectToQueryString = function(obj) {
      var str = [];
      angular.forEach(obj, function(value, key) {
        str.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
      });
      return str.join("&");
    };

    // This response_type MUST be passed to the authorization endpoint using
    // the implicit grant flow (4.2.1 of RFC 6749).
    var RESPONSE_TYPE = 'token';

    // Create a special object for config fields that are required and missing.
    // If any config items still contain it when Token is used, raise an error.
    var REQUIRED_AND_MISSING = {};

    var config = {
      clientId: REQUIRED_AND_MISSING,
      redirectUri: REQUIRED_AND_MISSING,
      authorizationEndpoint: REQUIRED_AND_MISSING,
      localStorageName: 'accessToken',
      verifyFunc: REQUIRED_AND_MISSING,
      scopes: []
    };

    this.extendConfig = function(configExtension) {
      config = angular.extend(config, configExtension);
    };

    this.$get = function($q, $http, $window, $rootScope) {
      var requiredAndMissing = [];
      angular.forEach(config, function(value, key) {
        if (value === REQUIRED_AND_MISSING) {
          requiredAndMissing.push(key);
        }
      });

      if (requiredAndMissing.length) {
        throw new Error("TokenProvider is insufficiently configured.  Please " +
          "configure the following options using " +
          "TokenProvider.extendConfig: " + requiredAndMissing.join(", "))
      }

      if (!config.clientId) {
        throw new Error("clientId needs to be configured using TokenProvider.");
      }

      var getParams = function() {
        // TODO: Facebook uses comma-delimited scopes. This is not compliant with section 3.3 but perhaps support later.

        return {
          response_type: config.responseType || RESPONSE_TYPE,
          client_id: config.clientId,
          redirect_uri: config.redirectUri,
          scope: config.scopes.join(" ")
        }
      };

      return {
        // TODO: get/set might want to support expiration to reauthenticate
        // TODO: check for localStorage support and otherwise perhaps use other methods of storing data (e.g. cookie)

        /**
         * Returns the stored access token.
         *
         * @returns {string} The access token.
         */
        get: function() {
          return localStorage[config.localStorageName];
        },

        /**
         * Persist the access token so that it can be retrieved later by.
         *
         * @param accessToken
         */
        set: function(accessToken) {
          localStorage[config.localStorageName] = accessToken;
        },

        /**
         * Verifies that the access token is was issued for the use of the current client.
         *
         * @param accessToken An access token received from the authorization server.
         * @returns {Promise} Promise that will be resolved when the authorization server has verified that the
         *  token is valid, and we've verified that the token is passed back has audience that matches our client
         *  ID (to prevent the Confused Deputy Problem).
         *
         *  If there's an error verifying the token, the promise is rejected with an object identifying the `name` error
         *  in the name member.  The `name` can be either:
         *
         *    - `invalid_audience`: The audience didn't match our client ID.
         *    - `error_response`: The server responded with an error, typically because the token was invalid.  In this
         *      case, the callback parameters to `error` callback on `$http` are available in the object (`data`,
         *      `status`, `headers`, `config`).
         */
        verifyAsync: function(accessToken) {
          return config.verifyFunc(config, accessToken);
        },

        /**
         * Verifies an access token asynchronously.
         *
         * @param extraParams An access token received from the authorization server.
         * @param popupOptions Settings for the display of the popup.
         * @returns {Promise} Promise that will be resolved when the authorization server has verified that the
         *  token is valid, and we've verified that the token is passed back has audience that matches our client
         *  ID (to prevent the Confused Deputy Problem).
         *
         *  If there's an error verifying the token, the promise is rejected with an object identifying the `name` error
         *  in the name member.  The `name` can be either:
         *
         *    - `invalid_audience`: The audience didn't match our client ID.
         *    - `error_response`: The server responded with an error, typically because the token was invalid.  In this
         *      case, the callback parameters to `error` callback on `$http` are available in the object (`data`,
         *      `status`, `headers`, `config`).
         */
        getTokenByPopup: function(extraParams, popupOptions) {
          popupOptions = angular.extend({
            name: 'AuthPopup',
            openParams: {
              width: 650,
              height: 300,
              resizable: true,
              scrollbars: true,
              status: true
            }
          }, popupOptions);

          var deferred = $q.defer(),
            params = angular.extend(getParams(), extraParams),
            url = config.authorizationEndpoint + '?' + objectToQueryString(params),
            resolved = false;

          var formatPopupOptions = function(options) {
            var pairs = [];
            angular.forEach(options, function(value, key) {
              if (value || value === 0) {
                value = value === true ? 'yes' : value;
                pairs.push(key + '=' + value);
              }
            });
            return pairs.join(',');
          };

          var popup = window.open(url, popupOptions.name, formatPopupOptions(popupOptions.openParams));

          // TODO: binding occurs for each reauthentication, leading to leaks for long-running apps.

          angular.element($window).bind('message', function(event) {
            if (event.source == popup && event.origin == window.location.origin) {
              $rootScope.$apply(function() {
                if (event.data.access_token) {
                  deferred.resolve(event.data)
                } else {
                  deferred.reject(event.data)
                }
              })
            }
          });

          // TODO: reject deferred if the popup was closed without a message being delivered + maybe offer a timeout

          return deferred.promise;
        }
      }
    }
  }).

  /**
   * A controller for the redirect endpoint that inspects the URL redirected to by the authorization server and sends
   * it back to other windows using.
   */
  controller('CallbackCtrl', function($scope, $location) {

    /**
     * Parses an escaped url query string into key-value pairs.
     *
     * (Copied from Angular.js in the AngularJS project.)
     *
     * @returns Object.<(string|boolean)>
     */
    function parseKeyValue(/**string*/keyValue) {
      var obj = {}, key_value, key;
      angular.forEach((keyValue || "").split('&'), function(keyValue){
        if (keyValue) {
          key_value = keyValue.split('=');
          key = decodeURIComponent(key_value[0]);
          obj[key] = angular.isDefined(key_value[1]) ? decodeURIComponent(key_value[1]) : true;
        }
      });
      return obj;
    }

    var queryString = $location.path().substring(1);  // preceding slash omitted
    var params = parseKeyValue(queryString);

    // TODO: The target origin should be set to an explicit origin.  Otherwise, a malicious site that can receive
    //       the token if it manages to change the location of the parent. (See:
    //       https://developer.mozilla.org/en/docs/DOM/window.postMessage#Security_concerns)

    window.opener.postMessage(params, "*");
    window.close();
  });
<|endoftext|>