<commit_before>// Copyright 2012 Iris Couch, all rights reserved.
//
// Test displaying DNS records

var util = require('util')

var parse = require('./parse')
var encode = require('./encode')
var constants = require('./constants')

module.exports = DNSMessage

var SECTIONS = ['question', 'answer', 'authority', 'additional']

// A DNS message.  This is an easy-to-understand object representation of
// standard DNS queries and responses.
//
// Attributes:
// * id                  - a number representing the unique query ID
// * type                - "request" or "response"
// * response            - Number (server response code)
// * opcode              - "query", "iquery", "status", "unassigned", "notify", "update"
// * authoritative       - Boolean
// * truncated           - Boolean
// * recursion_desired   - Boolean
// * recursion_available - Boolean
// * authenticated       - Boolean
// * checking_disabled   - Boolean
//
// Optional attributes:
// * question (optional) - Array of the question section
// * answer (optional) - Array of the answer section
// * authority (optional) - Array of the authority section
// * additional (optional) - Array of the additional section
//
// Methods:
// * toString() - return a human-readable representation of this message
// * toJSON() - Return a JSON-friendly represenation of this message
// * toBinary() - Return a buffer of the encoded message
function DNSMessage (body) {
  if(! Buffer.isBuffer(body))
    throw new Error('Must provide a buffer argument with message data')

  this.id = null
  this.type                = null
  this.responseCode        = null
  this.opcode              = null
  this.authoritative       = null
  this.truncated           = null
  this.recursion_desired   = null
  this.recursion_available = null
  this.authenticated       = null
  this.checking_disabled   = null

  this.parse(body)
}

DNSMessage.prototype.parse = function(body) {
  var self = this

  self.id = parse.id(body)

  var qr = parse.qr(body)
  self.type = (qr == 0) ? 'request' : 'response'

  self.responseCode = parse.rcode(body)

  var opcode_names = ['query', 'iquery', 'status', null, 'notify', 'update']
  var opcode = parse.opcode(body)
  self.opcode = opcode_names[opcode] || null

  self.authoritative       = !! parse.aa(body)
  self.truncated           = !! parse.tc(body)
  self.recursion_desired   = !! parse.rd(body)
  self.recursion_available = !! parse.ra(body)
  self.authenticated       = !! parse.ad(body)
  self.checking_disabled   = !! parse.cd(body)

  SECTIONS.forEach(function(section) {
    var count = parse.record_count(body, section)
    if(count) {
      self[section] = []
      for(var i = 0; i < count; i++)
        self[section].push(new DNSRecord(body, section, i))
    }
  })
}

DNSMessage.prototype.toBinary = function() {
  var self = this

  var state = new encode.State
  state.message(this)
  return state.toBinary()
}

DNSMessage.prototype.toString = function() {
  var self = this

  var info = [ util.format('ID                 : %d', self.id)
             , util.format("Type               : %s", self.type)
             , util.format("Opcode             : %s", self.opcode)
             , util.format("Authoritative      : %s", self.authoritative)
             , util.format("Truncated          : %s", self.truncated)
             , util.format("Recursion Desired  : %s", self.recursion_desired)
             , util.format("Recursion Available: %s", self.recursion_available)
             , util.format("Response Code      : %d", self.responseCode)
             ]

  SECTIONS.forEach(function(section) {
    if(self[section]) {
      info.push(util.format(';; %s SECTION:', section.toUpperCase()))
      self[section].forEach(function(record) {
        info.push(record.toString())
      })
    }
  })

  return info.join('\n')
}

// An individual record from a DNS message
//
// Attributes:
// * name  - Host name
// * type  - Query type ('A', 'NS', 'CNAME', etc. or 'Unknown')
// * class - Network class ('IN', 'None' 'Unknown')
// * ttl   - Time to live for the data in the record
// * data  - The record data value, or null if not applicable
function DNSRecord (body, section_name, record_num) {
  this.name = null
  this.type = null
  this.class = null

  // Leave these undefined for more consice and clear JSON serialization.
  //this.ttl  = null
  //this.data = null

  this.parse(body, section_name, record_num)
}

DNSRecord.prototype.parse = function(body, section_name, record_num) {
  var self = this

  self.name = parse.record_name(body, section_name, record_num)

  var clas = parse.record_class(body, section_name, record_num)
  self.class = constants.class_to_label(clas)
  if(! self.class)
    throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown class: ' + type)

  var type = parse.record_type(body, section_name, record_num)
  self.type = constants.type_to_label(type)
  if(! self.type)
    throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown type: ' + type)

  if(section_name == 'question')
    return

  self.ttl  = parse.record_ttl(body, section_name, record_num)

  var rdata = parse.record_data(body, section_name, record_num)
  if(self.type == 'A' && rdata.length == 4)
    self.data = inet_ntoa(rdata)
  else if(~ ['NS', 'CNAME', 'SOA', 'PTR'].indexOf(self.type))
    self.data = parse.uncompress(body, rdata)
  else if(self.type == 'MX')
    self.data = parse.mx(body, rdata)
  else
    self.data = Array.prototype.slice.call(rdata)
}

DNSRecord.prototype.toString = function() {
  var self = this
  return [ width(23, self.name)
         , width( 7, self.ttl || '')
         , width( 7, self.class)
         , width( 7, self.type)
         , self.type == 'MX' && self.data
            ? (width(3, self.data[0]) + ' ' + self.data[1])
           : Buffer.isBuffer(self.data)
            ? self.data.toString('hex')
            : self.data || ''
         ].join(' ')
}

//
// Utilities
//

function width(str_len, str) {
  str = '' + str
  do {
    var needed = str_len - str.length
    if(needed > 0)
      str = ' ' + str
  } while(needed > 0)

  return str
}

function inet_ntoa(buf) {
  return buf[0] + '.' + buf[1] + '.' + buf[2] + '.' + buf[3]
}
<commit_msg>Beginning to clean up messages before encoding them<commit_after>// Copyright 2012 Iris Couch, all rights reserved.
//
// Test displaying DNS records

var util = require('util')

var parse = require('./parse')
var encode = require('./encode')
var constants = require('./constants')

module.exports = DNSMessage

var SECTIONS = ['question', 'answer', 'authority', 'additional']

// A DNS message.  This is an easy-to-understand object representation of
// standard DNS queries and responses.
//
// Attributes:
// * id                  - a number representing the unique query ID
// * type                - "request" or "response"
// * response            - Number (server response code)
// * opcode              - "query", "iquery", "status", "unassigned", "notify", "update"
// * authoritative       - Boolean
// * truncated           - Boolean
// * recursion_desired   - Boolean
// * recursion_available - Boolean
// * authenticated       - Boolean
// * checking_disabled   - Boolean
//
// Optional attributes:
// * question (optional) - Array of the question section
// * answer (optional) - Array of the answer section
// * authority (optional) - Array of the authority section
// * additional (optional) - Array of the additional section
//
// Methods:
// * toString() - return a human-readable representation of this message
// * toJSON() - Return a JSON-friendly represenation of this message
// * toBinary() - Return a buffer of the encoded message
function DNSMessage (body) {
  if(! Buffer.isBuffer(body))
    throw new Error('Must provide a buffer argument with message data')

  this.id = null
  this.type                = null
  this.responseCode        = null
  this.opcode              = null
  this.authoritative       = null
  this.truncated           = null
  this.recursion_desired   = null
  this.recursion_available = null
  this.authenticated       = null
  this.checking_disabled   = null

  this.parse(body)
}

DNSMessage.prototype.parse = function(body) {
  var self = this

  self.id = parse.id(body)

  var qr = parse.qr(body)
  self.type = (qr == 0) ? 'request' : 'response'

  self.responseCode = parse.rcode(body)

  var opcode_names = ['query', 'iquery', 'status', null, 'notify', 'update']
  var opcode = parse.opcode(body)
  self.opcode = opcode_names[opcode] || null

  self.authoritative       = !! parse.aa(body)
  self.truncated           = !! parse.tc(body)
  self.recursion_desired   = !! parse.rd(body)
  self.recursion_available = !! parse.ra(body)
  self.authenticated       = !! parse.ad(body)
  self.checking_disabled   = !! parse.cd(body)

  SECTIONS.forEach(function(section) {
    var count = parse.record_count(body, section)
    if(count) {
      self[section] = []
      for(var i = 0; i < count; i++)
        self[section].push(new DNSRecord(body, section, i))
    }
  })
}

DNSMessage.prototype.toBinary = function() {
  // The encoder is picky, so make sure it gets a valid message.
  var msg = JSON.parse(JSON.stringify(this))

  // Make sure records promising data have data.
  SECTIONS.forEach(function(section) {
    if(section == 'question')
      return

    msg[section] = msg[section] || []
    msg[section].forEach(function(record) {
      if(record.class == 'IN' && record.type == 'A')
        record.data = record.data || '0.0.0.0'
    })
  })

  var state = new encode.State
  state.message(msg)
  return state.toBinary()
}

DNSMessage.prototype.toString = function() {
  var self = this

  var info = [ util.format('ID                 : %d', self.id)
             , util.format("Type               : %s", self.type)
             , util.format("Opcode             : %s", self.opcode)
             , util.format("Authoritative      : %s", self.authoritative)
             , util.format("Truncated          : %s", self.truncated)
             , util.format("Recursion Desired  : %s", self.recursion_desired)
             , util.format("Recursion Available: %s", self.recursion_available)
             , util.format("Response Code      : %d", self.responseCode)
             ]

  SECTIONS.forEach(function(section) {
    if(self[section]) {
      info.push(util.format(';; %s SECTION:', section.toUpperCase()))
      self[section].forEach(function(record) {
        info.push(record.toString())
      })
    }
  })

  return info.join('\n')
}

// An individual record from a DNS message
//
// Attributes:
// * name  - Host name
// * type  - Query type ('A', 'NS', 'CNAME', etc. or 'Unknown')
// * class - Network class ('IN', 'None' 'Unknown')
// * ttl   - Time to live for the data in the record
// * data  - The record data value, or null if not applicable
function DNSRecord (body, section_name, record_num) {
  this.name = null
  this.type = null
  this.class = null

  // Leave these undefined for more consice and clear JSON serialization.
  //this.ttl  = null
  //this.data = null

  this.parse(body, section_name, record_num)
}

DNSRecord.prototype.parse = function(body, section_name, record_num) {
  var self = this

  self.name = parse.record_name(body, section_name, record_num)

  var clas = parse.record_class(body, section_name, record_num)
  self.class = constants.class_to_label(clas)
  if(! self.class)
    throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown class: ' + type)

  var type = parse.record_type(body, section_name, record_num)
  self.type = constants.type_to_label(type)
  if(! self.type)
    throw new Error('Record '+record_num+' in section "'+section_name+'" has unknown type: ' + type)

  if(section_name == 'question')
    return

  self.ttl  = parse.record_ttl(body, section_name, record_num)

  var rdata = parse.record_data(body, section_name, record_num)
  if(self.type == 'A' && rdata.length == 4)
    self.data = inet_ntoa(rdata)
  else if(~ ['NS', 'CNAME', 'SOA', 'PTR'].indexOf(self.type))
    self.data = parse.uncompress(body, rdata)
  else if(self.type == 'MX')
    self.data = parse.mx(body, rdata)
  else
    self.data = Array.prototype.slice.call(rdata)
}

DNSRecord.prototype.toString = function() {
  var self = this
  return [ width(23, self.name)
         , width( 7, self.ttl || '')
         , width( 7, self.class)
         , width( 7, self.type)
         , self.type == 'MX' && self.data
            ? (width(3, self.data[0]) + ' ' + self.data[1])
           : Buffer.isBuffer(self.data)
            ? self.data.toString('hex')
            : self.data || ''
         ].join(' ')
}

//
// Utilities
//

function width(str_len, str) {
  str = '' + str
  do {
    var needed = str_len - str.length
    if(needed > 0)
      str = ' ' + str
  } while(needed > 0)

  return str
}

function inet_ntoa(buf) {
  return buf[0] + '.' + buf[1] + '.' + buf[2] + '.' + buf[3]
}
<|endoftext|>


<commit_before>define(function(require, exports, module) {
  main.consumes = [
    "Plugin", "c9", "tabManager", "preferences", "settings", "save", "ace", "proc", "info", "http"
  ];
  main.provides = ["wakatime"];
  return main;

  function main(options, imports, register) {
    var Plugin = imports.Plugin;
    var c9 = imports.c9;
    var settings = imports.settings;
    var prefs = imports.preferences;
    var tabManager = imports.tabManager;
    var ace = imports.ace;
    var save = imports.save;
    var proc = imports.proc;
    var info = imports.info;
    var http = imports.http;

    var path = require('path');

    /***** Initialization *****/

    var plugin = new Plugin("WakaTime", main.consumes);
    var api  = plugin.setAPIKey(options.apikey);
    // var emit = plugin.getEmitter();
    // emit.setMaxListeners(2);

    var pluginVersion = null;
    var c9Version = null;
    var lastFile = null;
    var lastWrite = null;
    var lastTime = 0;
    var cachedPythonLocation = null;
    var cachedApiKey = null;

    function init() {
      pluginVersion = options.version || '3.0.0';
      c9Version = c9.version.split(' ')[0];
      if (settings.get('user/wakatime/@debug'))
        console.log('Initializing WakaTime v' + pluginVersion);

      getApiKey(function(apiKey) {
        if (isValidApiKey(apiKey))
          settings.set("user/wakatime/@apikey", apiKey);
        setupSettings();
      });

      // get user's c9 email address
      info.getUser(function(err, user) {
        if (err || !user || !user.email) {
          console.log(err);
          finishInit();
        } else {
          getApiKey(function(apiKey) {
            if (isValidApiKey(apiKey)) {
              finishInit();
            } else {
              createWakaUser(user, finishInit);
            }
          });
        }
      });
    }

    function finishInit() {

      // make sure we have a WakaTime api key
      getApiKey(function(apiKey) {

        if (!isValidApiKey(apiKey)) {
          apiKey = promptForApiKey(apiKey);
          if (isValidApiKey(apiKey))
            setApiKey(apiKey);
        }

        setupEventHandlers();

      });
    }

    /***** Methods *****/

    function createWakaUser(user, callback) {
      var url = 'https://wakatime.com/api/v1/users/signup/c9'
      var body = {
        email: user.email,
      };
      if (user.fullname) body.full_name = user.fullname;
      var options = {
        method: 'POST',
        body: body,
        timeout: 30000,
      };
      http.request(url, options, function(err, data, res) {
        if (err) console.log(err);
        if (data && data.data && isValidApiKey(data.data.api_key))
          setApiKey(data.data.api_key);
        callback && callback();
      });
    }

    function setApiKey(apiKey) {
      if (isValidApiKey(apiKey)) {
        cachedApiKey = apiKey;

        var data = {
          apiKey: apiKey,
        };
        api.setPersistentData("user", data, function(err) {
          if (err) console.log(err);
        });
      }
    }

    function getApiKey(callback) {
      if (cachedApiKey) return callback && callback(cachedApiKey);

      api.getPersistentData("user", function(err, data) {
        if (err) console.log(err);
        var apiKey = undefined;
        if (data && data.apiKey) apiKey = data.apiKey;
        callback && callback(apiKey);
      });
    }

    function promptForApiKey(defaultKey) {
      var key = window.prompt("[WakaTime] Enter your wakatime.com api key:", defaultKey);
      return key;
    }

    function isValidApiKey(key) {
      if (!key) return false;
      var re = new RegExp('^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i');
      return re.test(key);
    }

    function setupSettings() {
      settings.on("read", function(e) {
        settings.setDefaults("user/wakatime", [
          ["apikey", ""],
          ["debug", false],
          ["exclude", ""],
        ]);
      });
      settings.on("user/wakatime/@apikey", function(value) {
          setApiKey(value);
      }, plugin);
      prefs.add({
        "WakaTime" : {
          position: 650,
          "WakaTime" : {
            position: 100,
            "API Key": {
              type: "textbox",
              setting: "user/wakatime/@apikey",
              position: 100,
              width: 242,
            },
            "Debug": {
              type: "checkbox",
              setting: "user/wakatime/@debug",
              position: 200,
            },
            "Exclude": {
              type: "textarea-row",
              setting: "user/wakatime/@exclude",
              position: 300,
              width: 600,
              height: 100,
              fixedFont: true,
              rowheight: 250,
            },
          }
        }
      }, plugin);
    }

    function setupEventHandlers() {

      save.on('afterSave', function(e) {
        handleActivity(true);
      });

      tabManager.on('focus', function(e) {
        handleActivity();
      });

      ace.on('create', function(createEvent) {
        createEvent.editor.ace.on('input', function(e) {
          handleActivity();
        });
      }, plugin);
    }

    function enoughTimePassed(time) {
      return lastTime + 120000 < time;
    }

    function fileIsIgnored(file) {
      var patterns = settings.get('user/wakatime/@exclude').split(/\n/);
      var ignore = false;
      for (var i=0; i<patterns.length; i++) {
        if (patterns[i].trim()) {
          var re = new RegExp(patterns[i], 'gi');
          if (re.test(file)) {
            // console.log("matched " + patterns[i] + " on " + file);
            ignore = true;
            break;
          }
        }
      }
      return ignore;
    }

    function pythonLocation(callback, locations) {
      if (cachedPythonLocation) {
        if (callback)
          return callback(cachedPythonLocation);
        else
          return;
      }

      if (locations === undefined) {
        locations = [
          'pythonw',
          'python',
          '/usr/local/bin/python',
          '/usr/bin/python',
        ];
        for (var i=26; i<40; i++) {
          locations.push(path.join('python' + i, 'pythonw'));
          locations.push(path.join('Python' + i, 'pythonw'));
        }
      }

      if (locations.length == 0) {
        if (callback)
          callback(null);
        return;
      }

      var args = ['--version'];
      var location = locations[0];

      proc.execFile(location, {args:args}, function(error, stdout, stderr) {
        if (!error) {
          cachedPythonLocation = location;
          if (callback)
            callback(location);
        } else {
          locations.splice(0, 1);
          pythonLocation(callback, locations);
        }
      });

    }

    function coreLocation() {
      return path.join(c9.home, '.c9/lib/wakatime/wakatime/cli.py');
    }

    function obfuscateKey(key) {
      var newKey = '';
      if (key) {
        newKey = key;
        if (key.length > 4)
          newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
      }
      return newKey;
    }

    function obfuscateKeyFromArguments(args) {
      var newCmds = [];
      var lastCmd = "";
      for (var i = 0; i<args.length; i++) {
        if (lastCmd == "--key")
          newCmds.push(obfuscateKey(args[i]));
        else
          newCmds.push(args[i]);
        lastCmd = args[i];
      }
      return newCmds;
    }

    function handleActivity(isWrite) {
      var tab = tabManager.focussedTab;
      if (!tab) return;

      var file = tab.path;
      if (!file)
        return;

      if (file.indexOf('~') == 0) {
        file = path.join(c9.home, file.substring(1));
      } else if (file.indexOf(c9.home, 0) != 0) {
        file = path.join(c9.workspaceDir, file);
      }

      var time = Date.now();
      if (isWrite && lastWrite != isWrite || enoughTimePassed(time) || lastFile != file) {
        if (fileIsIgnored(file))
          return;

        var cursorpos = getCursorPosition(tab);
        sendHeartbeat(file, time, isWrite, cursorpos);
      }
    }

    function getCursorPosition(tab) {
      try {
        if (!tab) return null;
        var aceSession = tab.document.getSession().session;
        if (!aceSession) return null;
        var aceDoc = aceSession.doc;
        if (!aceDoc) return null;
        var selection = aceSession.getSelection();
        if (!selection) return null;
        var anchor = selection.anchor;
        if (!anchor) return null;
        var row = anchor.row;
        var col = anchor.column;
        var cursorpos = aceDoc.positionToIndex({ row: row, column: col });
        if (cursorpos !== undefined && cursorpos != null) return cursorpos;
        return null;
      } catch(err) {
        console.log(err);
        cursorpos = null;
      }
    }

    function sendHeartbeat(file, time, isWrite, cursorpos) {
      pythonLocation(function(python) {
        if (!python)
          return;
        var debug = settings.get("user/wakatime/@debug");
        getApiKey(function(apiKey) {
          var core = coreLocation();
          var userAgent = 'c9/' + c9Version + ' c9-wakatime/' + pluginVersion;
          var args = [core, '--file', file, '--plugin', userAgent];
          if (isWrite)
            args.push('--write');
          if (isValidApiKey(apiKey)) {
            args.push('--key');
            args.push(apiKey);
          }
          if (cursorpos != null) {
            args.push('--cursorpos');
            args.push(cursorpos);
          }
          if (debug) {
            args.push('--verbose');
            var clone = args.slice(0);
            clone.unshift(python);
            console.log('Sending heartbeat: ' + obfuscateKeyFromArguments(clone).join(' '));
          }
          proc.execFile(python, {args:args}, function(error, stdout, stderr) {
            if (error) {
              if (stderr && stderr != '')
                console.warn(stderr);
              if (stdout && stdout != '')
                console.warn(stdout);

              var msg = error.message;
              var status = 'Error'
              var title = 'Unknown Error (' + error.code + '); Check your Dev Console and ~/.wakatime.log for more info.'
              if (error.code == 102) {
                msg = null;
                status = null;
                title = 'WakaTime Offline, coding activity will sync when online.';
              } else if (error.code == 103) {
                msg = 'An error occured while parsing ~/.wakatime.cfg. Check ~/.wakatime.log for more info.';
                status = 'Error';
                title = msg;
              } else if (error.code == 104) {
                msg = 'Invalid API Key. Make sure your API Key is correct!';
                status = 'Error';
                title = msg;
              }

              console.warn(msg);
              setStatusBarText(status);
              setStatusBarTitle(title);
            }
          });
          lastTime = time;
          lastFile = file;
          lastWrite = !!isWrite;
        });
      });
    }

    function setStatusBarText(text) {
      // TODO: update status bar text
      return;
    }

    function setStatusBarTitle(text) {
      // TODO: update status bar onhover title
      return;
    }

    /***** Lifecycle *****/

    plugin.on("load", function() {
      init();
    });
    plugin.on("unload", function() {
    });

    /***** Register and define API *****/
    plugin.freezePublicAPI({
      _events: [
      ],
    });

    register(null, {
      "wakatime": plugin
    });
  }
});
<commit_msg>rename api key methods to prevent confusion between plugin.setAPIKey<commit_after>define(function(require, exports, module) {
  main.consumes = [
    "Plugin", "c9", "tabManager", "preferences", "settings", "save", "ace", "proc", "info", "http"
  ];
  main.provides = ["wakatime"];
  return main;

  function main(options, imports, register) {
    var Plugin = imports.Plugin;
    var c9 = imports.c9;
    var settings = imports.settings;
    var prefs = imports.preferences;
    var tabManager = imports.tabManager;
    var ace = imports.ace;
    var save = imports.save;
    var proc = imports.proc;
    var info = imports.info;
    var http = imports.http;

    var path = require('path');

    /***** Initialization *****/

    var plugin = new Plugin("WakaTime", main.consumes);
    var api  = plugin.setAPIKey(options.apikey);
    // var emit = plugin.getEmitter();
    // emit.setMaxListeners(2);

    var pluginVersion = null;
    var c9Version = null;
    var lastFile = null;
    var lastWrite = null;
    var lastTime = 0;
    var cachedPythonLocation = null;
    var cachedApiKey = null;

    function init() {
      pluginVersion = options.version || '3.0.0';
      c9Version = c9.version.split(' ')[0];
      if (settings.get('user/wakatime/@debug'))
        console.log('Initializing WakaTime v' + pluginVersion);

      getWakaApiKey(function(apiKey) {
        if (isValidApiKey(apiKey))
          settings.set("user/wakatime/@apikey", apiKey);
        setupSettings();
      });

      // get user's c9 email address
      info.getUser(function(err, user) {
        if (err || !user || !user.email) {
          console.log(err);
          finishInit();
        } else {
          getWakaApiKey(function(apiKey) {
            if (isValidApiKey(apiKey)) {
              finishInit();
            } else {
              createWakaUser(user, finishInit);
            }
          });
        }
      });
    }

    function finishInit() {

      // make sure we have a WakaTime api key
      getWakaApiKey(function(apiKey) {

        if (!isValidApiKey(apiKey)) {
          apiKey = promptForApiKey(apiKey);
          if (isValidApiKey(apiKey))
            setWakaApiKey(apiKey);
        }

        setupEventHandlers();

      });
    }

    /***** Methods *****/

    function createWakaUser(user, callback) {
      var url = 'https://wakatime.com/api/v1/users/signup/c9'
      var body = {
        email: user.email,
      };
      if (user.fullname) body.full_name = user.fullname;
      var options = {
        method: 'POST',
        body: body,
        timeout: 30000,
      };
      http.request(url, options, function(err, data, res) {
        if (err) console.log(err);
        if (data && data.data && isValidApiKey(data.data.api_key))
          setWakaApiKey(data.data.api_key);
        callback && callback();
      });
    }

    function setWakaApiKey(apiKey) {
      if (isValidApiKey(apiKey)) {
        cachedApiKey = apiKey;

        var data = {
          apiKey: apiKey,
        };
        api.setPersistentData("user", data, function(err) {
          if (err) console.log(err);
        });
      }
    }

    function getWakaApiKey(callback) {
      if (cachedApiKey) return callback && callback(cachedApiKey);

      api.getPersistentData("user", function(err, data) {
        if (err) console.warn(err);
        var apiKey = undefined;
        if (data && data.apiKey) apiKey = data.apiKey;
        callback && callback(apiKey);
      });
    }

    function promptForApiKey(defaultKey) {
      var key = window.prompt("[WakaTime] Enter your wakatime.com api key:", defaultKey);
      return key;
    }

    function isValidApiKey(key) {
      if (!key) return false;
      var re = new RegExp('^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i');
      return re.test(key);
    }

    function setupSettings() {
      settings.on("read", function(e) {
        settings.setDefaults("user/wakatime", [
          ["apikey", ""],
          ["debug", false],
          ["exclude", ""],
        ]);
      });
      settings.on("user/wakatime/@apikey", function(value) {
        setWakaApiKey(value);
      }, plugin);
      prefs.add({
        "WakaTime" : {
          position: 650,
          "WakaTime" : {
            position: 100,
            "API Key": {
              type: "textbox",
              setting: "user/wakatime/@apikey",
              position: 100,
              width: 242,
            },
            "Debug": {
              type: "checkbox",
              setting: "user/wakatime/@debug",
              position: 200,
            },
            "Exclude": {
              type: "textarea-row",
              setting: "user/wakatime/@exclude",
              position: 300,
              width: 600,
              height: 100,
              fixedFont: true,
              rowheight: 250,
            },
          }
        }
      }, plugin);
    }

    function setupEventHandlers() {

      save.on('afterSave', function(e) {
        handleActivity(true);
      });

      tabManager.on('focus', function(e) {
        handleActivity();
      });

      ace.on('create', function(createEvent) {
        createEvent.editor.ace.on('input', function(e) {
          handleActivity();
        });
      }, plugin);
    }

    function enoughTimePassed(time) {
      return lastTime + 120000 < time;
    }

    function fileIsIgnored(file) {
      var patterns = settings.get('user/wakatime/@exclude').split(/\n/);
      var ignore = false;
      for (var i=0; i<patterns.length; i++) {
        if (patterns[i].trim()) {
          var re = new RegExp(patterns[i], 'gi');
          if (re.test(file)) {
            // console.log("matched " + patterns[i] + " on " + file);
            ignore = true;
            break;
          }
        }
      }
      return ignore;
    }

    function pythonLocation(callback, locations) {
      if (cachedPythonLocation) {
        if (callback)
          return callback(cachedPythonLocation);
        else
          return;
      }

      if (locations === undefined) {
        locations = [
          'pythonw',
          'python',
          '/usr/local/bin/python',
          '/usr/bin/python',
        ];
        for (var i=26; i<40; i++) {
          locations.push(path.join('python' + i, 'pythonw'));
          locations.push(path.join('Python' + i, 'pythonw'));
        }
      }

      if (locations.length == 0) {
        if (callback)
          callback(null);
        return;
      }

      var args = ['--version'];
      var location = locations[0];

      proc.execFile(location, {args:args}, function(error, stdout, stderr) {
        if (!error) {
          cachedPythonLocation = location;
          if (callback)
            callback(location);
        } else {
          locations.splice(0, 1);
          pythonLocation(callback, locations);
        }
      });

    }

    function coreLocation() {
      return path.join(c9.home, '.c9/lib/wakatime/wakatime/cli.py');
    }

    function obfuscateKey(key) {
      var newKey = '';
      if (key) {
        newKey = key;
        if (key.length > 4)
          newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
      }
      return newKey;
    }

    function obfuscateKeyFromArguments(args) {
      var newCmds = [];
      var lastCmd = "";
      for (var i = 0; i<args.length; i++) {
        if (lastCmd == "--key")
          newCmds.push(obfuscateKey(args[i]));
        else
          newCmds.push(args[i]);
        lastCmd = args[i];
      }
      return newCmds;
    }

    function handleActivity(isWrite) {
      var tab = tabManager.focussedTab;
      if (!tab) return;

      var file = tab.path;
      if (!file)
        return;

      if (file.indexOf('~') == 0) {
        file = path.join(c9.home, file.substring(1));
      } else if (file.indexOf(c9.home, 0) != 0) {
        file = path.join(c9.workspaceDir, file);
      }

      var time = Date.now();
      if (isWrite && lastWrite != isWrite || enoughTimePassed(time) || lastFile != file) {
        if (fileIsIgnored(file))
          return;

        var cursorpos = getCursorPosition(tab);
        sendHeartbeat(file, time, isWrite, cursorpos);
      }
    }

    function getCursorPosition(tab) {
      try {
        if (!tab) return null;
        var aceSession = tab.document.getSession().session;
        if (!aceSession) return null;
        var aceDoc = aceSession.doc;
        if (!aceDoc) return null;
        var selection = aceSession.getSelection();
        if (!selection) return null;
        var anchor = selection.anchor;
        if (!anchor) return null;
        var row = anchor.row;
        var col = anchor.column;
        var cursorpos = aceDoc.positionToIndex({ row: row, column: col });
        if (cursorpos !== undefined && cursorpos != null) return cursorpos;
        return null;
      } catch(err) {
        console.log(err);
        cursorpos = null;
      }
    }

    function sendHeartbeat(file, time, isWrite, cursorpos) {
      pythonLocation(function(python) {
        if (!python)
          return;
        var debug = settings.get("user/wakatime/@debug");
        getWakaApiKey(function(apiKey) {
          var core = coreLocation();
          var userAgent = 'c9/' + c9Version + ' c9-wakatime/' + pluginVersion;
          var args = [core, '--file', file, '--plugin', userAgent];
          if (isWrite)
            args.push('--write');
          if (isValidApiKey(apiKey)) {
            args.push('--key');
            args.push(apiKey);
          }
          if (cursorpos != null) {
            args.push('--cursorpos');
            args.push(cursorpos);
          }
          if (debug) {
            args.push('--verbose');
            var clone = args.slice(0);
            clone.unshift(python);
            console.log('Sending heartbeat: ' + obfuscateKeyFromArguments(clone).join(' '));
          }
          proc.execFile(python, {args:args}, function(error, stdout, stderr) {
            if (error) {
              if (stderr && stderr != '')
                console.warn(stderr);
              if (stdout && stdout != '')
                console.warn(stdout);

              var msg = error.message;
              var status = 'Error'
              var title = 'Unknown Error (' + error.code + '); Check your Dev Console and ~/.wakatime.log for more info.'
              if (error.code == 102) {
                msg = null;
                status = null;
                title = 'WakaTime Offline, coding activity will sync when online.';
              } else if (error.code == 103) {
                msg = 'An error occured while parsing ~/.wakatime.cfg. Check ~/.wakatime.log for more info.';
                status = 'Error';
                title = msg;
              } else if (error.code == 104) {
                msg = 'Invalid API Key. Make sure your API Key is correct!';
                status = 'Error';
                title = msg;
              }

              console.warn(msg);
              setStatusBarText(status);
              setStatusBarTitle(title);
            }
          });
          lastTime = time;
          lastFile = file;
          lastWrite = !!isWrite;
        });
      });
    }

    function setStatusBarText(text) {
      // TODO: update status bar text
      return;
    }

    function setStatusBarTitle(text) {
      // TODO: update status bar onhover title
      return;
    }

    /***** Lifecycle *****/

    plugin.on("load", function() {
      init();
    });
    plugin.on("unload", function() {
    });

    /***** Register and define API *****/
    plugin.freezePublicAPI({
      _events: [
      ],
    });

    register(null, {
      "wakatime": plugin
    });
  }
});
<|endoftext|>


<commit_before>var models = require('../../models');
var util = require('./util');
var passport = require('passport');
var log = require('../../logging');
var tagsUtil = require('./tagsUtil');
var requestUtil = require('./requestUtil');
var responseUtil = require('./responseUtil');
var multiparty = require('multiparty');
var config = require('../../config/config')
var uuidv4 = require('uuid/v4');
var jsonwebtoken = require('jsonwebtoken');
var sequelize = require('sequelize');
var modelsUtil = require('../../models/util/util');

module.exports = (app, baseUrl) => {
  var apiUrl = baseUrl + '/recordings';

  /**
   * @api {post} /api/v1/recordings Add a new recording.
   * @apiName PostRecording
   * @apiGroup Recordings
   * @apiDescription This call is used for uploads of new recordings. It
   * currently supports raw thermal video but will eventually support all
   * recording types. It takes a 'data' field which contains a JSON object
   * string that can contain any of the following fields, Note that 'type'
   * is required:
   * - type: REQUIRED, one of ('thermalRaw')
   * - duration
   * - recordingDateTime
   * - location
   * - version
   * - batteryCharging
   * - batteryLevel
   * - airplaneModeOn
   * - additionalMetadata
   * - processingMeta
   *
   * @apiUse V1DeviceAuthorizationHeader
   *
   * @apiParam {JSON} data Metadata about the recording (see above).
   * @apiParam {File} file File of the recording.
   *
   * @apiUse V1ResponseSuccess
   * @apiuse V1ResponseError
   */
  app.post(
    apiUrl,
    passport.authenticate(['jwt'], { session: false }),
    async (request, response) => {
      log.info(request.method + " Request: " + request.url);

      // Check that the request was authenticated by a device.
      if (!requestUtil.isFromADevice(request))
        return responseUtil.notFromADevice(response);
      var device = request.user;

      var recording;
      var key = uuidv4();
      var form = new multiparty.Form();
      var validData = false;
      var uploadProcess = null;
      var uploadStarted = false;
      var invalidMessages = [];

      // Make new Recording from the data field and Device.
      // TODO Stop stream if 'data' is invalid.
      form.on('field', (name, value) => {
        if (name != 'data') return; // Only parse data field.
        try {
          var data = JSON.parse(value);
          recording = models.Recording.build(data, {
            fields: models.Recording.apiSettableFields,
          });
          recording.set('rawFileKey', key);
          recording.set('DeviceId', device.id);
          recording.set('GroupId', device.GroupId);
          recording.set('processingState',
            models.Recording.processingStates[data.type][0]);
          if (typeof device.public === 'boolean')
            recording.set('public', device.public);
          validData = true;
        } catch (e) {
          log.debug(e);
          // TODO Stop stream here.
        }
      });

      // Stream file to LeoFS.
      form.on('part', (part) => {
        if (part.name != 'file') return part.resume();
        uploadStarted = true;
        log.debug('Streaming file to LeoFS.')
        uploadPromise = new Promise(function(resolve, reject) {
          var s3 = modelsUtil.openS3();
          s3.upload({
            Bucket: config.s3.bucket,
            Key: key,
            Body: part,
          }, (err, data) => {
            if (err) return reject(err);
            log.info("Finished streaming file to LeoFS Key:", key);
            resolve();
          });
        });
      });

      // Close response.
      form.on('close', async () => {
        log.info("Finished POST request.");
        if (!validData || !uploadStarted)
          return responseUtil.invalidDatapointUpload(response);

        // Check that th file uploaded to LeoFS.
        try {
          await uploadPromise;
        } catch (e) {
          return responseUtil.send(response, {
            statusCode: 500,
            success: false,
            messages: ["Failed to upload file to LeoFS"],
          });
        }
        // Validate and upload recording
        await recording.validate();
        await recording.save();
        return responseUtil.validDatapointUpload(response);
      });

      form.on('error', (e) => {
        log.error(e);
      })

      form.parse(request);
    });

  /**
   * @api {get} /api/v1/recordings Query available recordings
   * @apiName GetRecordigns
   * @apiGroup Recordings
   *
   * @apiUse V1UserAuthorizationHeader
   *
   * @apiParam {JSON} where [Sequelize where conditions](http://docs.sequelizejs.com/manual/tutorial/querying.html#where) for query
   * @apiParam {Number} offset Query result offset (for paging).
   * @apiParam {Number} limit Query result limit (for paging).
   * @apiParam {JSON} [order] [Sequelize ordering](http://docs.sequelizejs.com/manual/tutorial/querying.html#ordering). Exampel: [["recordingDateTime", "ASC"]]
   *
   * @apiUse V1ResponseSuccess
   * @apiSuccess {Number} offset Mirrors request offset parameter.
   * @apiSuccess {Number} limit Mirrors request limit parameter.
   * @apiSuccess {Number} count Total number of records which match the query.
   * @apiSuccess {JSON} rows List of details for records which matched the query.
   *
   * @apiUse V1ResponseError
   */
  app.get(
    apiUrl,
    passport.authenticate(['jwt'], { session: false }),
    async (request, response) => {
      log.info(request.method + " Request: " + request.url);

      // Check that the request was authenticated by a User.
      if (!requestUtil.isFromAUser(request))
        return responseUtil.notFromAUser(response);

      var where = request.query.where;
      var offset = parseInt(request.query.offset);
      var limit = parseInt(request.query.limit);
      var order = request.query.order;

      // Validate 'where', 'offset', and 'limit'
      var errorMessages = [];
      try {
        where = JSON.parse(where);
      } catch (e) {
        errorMessages.push("'where' field was not a valid JSON.")
      }
      if (isNaN(offset)) errorMessages.push("'offset' was not a number.")
      if (isNaN(limit)) errorMessages.push("'limit' was not a number.")
      if (order != null) {
        try {
          order = JSON.parse(order);
        } catch (e) {
          errorMessages.push("'order' was not a valid JSON.")
        }
      } else {
        order = [
          // Sort by recordingDatetime but handle the case of the
          // timestamp being missing and fallback to sorting by id.
          [sequelize.fn("COALESCE", sequelize.col('recordingDateTime'), '1970-01-01'), "DESC"],
          ["id", "DESC"],
        ];
      }
      if (errorMessages.length > 0) {
        return responseUtil.send(response, {
          statusCode: 400,
          success: false,
          messages: errorMessages,
        });
      }

      // Make sequelize query.
      var userGroupIds = await request.user.getGroupsIds();
      var query = {
        where: {
          "$and": [
            where, // User query
            { "$or": [{ public: true }, { GroupId: { "$in": userGroupIds } }] }
          ],
        },
        order: order,
        include: [
          { model: models.Group },
          { model: models.Tag },
          { model: models.Device, where: {}, attributes: ["devicename", "id"] },
        ],
        limit: limit,
        offset: offset,
        attributes: models.Recording.userGetAttributes,
      };
      // Query database.
      var result = await models.Recording.findAndCount(query);

      // Send response
      return responseUtil.send(response, {
        statusCode: 200,
        success: true,
        messages: ["Completed query."],
        limit: limit,
        offset: offset,
        count: result.count,
        rows: result.rows,
      });
    });

    /**
    * @api {get} /api/v1/recordings/:id Get a recording
    * @apiName GetRecording
    * @apiGroup Recordings
    * @apiDescription This call returns metadata for a recording in JSON format
    * and a JSON Web Token (JWT) which can be used to retrieve the recorded
    * content. This is should be used with the
    * [/api/v1/signedUrl API](#api-SignedUrl-GetFile).
    *
    * @apiUse V1UserAuthorizationHeader
    *
    * @apiUse V1ResponseSuccess
    * @apiSuccess {String} downloadFileJWT JSON Web Token to use to download the
    * recording file.
    * @apiSuccess {String} downloadRawJWT JSON Web Token to use to download
    * the raw recording data.
    * @apiSuccess {JSON} recording The recording data.
    *
    * @apiUse V1ResponseError
    */
    app.get(
      apiUrl + '/:id',
      passport.authenticate(['jwt'], { session: false }),
      async (request, response) => {
        log.info(request.method + " Request: " + request.url);

        // Check that the request was authenticated by a User.
        if (!requestUtil.isFromAUser(request))
          return responseUtil.notFromAUser(response);

        var id = parseInt(request.params.id);
        if (!id)
          return responseUtil.invalidDataId(response);

        // Make sequelize query.
        var userGroupIds = await request.user.getGroupsIds();
        var query = {
          where: {
            "$and": [
              { id: id },
              { "$or": [{ public: true }, { GroupId: { "$in": userGroupIds } }] }
            ],
          },
          include: [
            { model: models.Tag, },
            { model: models.Device, where: {}, attributes: ["devicename", "id"] },
          ],
          attributes: models.Recording.userGetAttributes.concat(['rawFileKey']),
        };

        // Query database.
        var recording = await models.Recording.findOne(query);

        downloadFileData = {
          _type: 'fileDownload',
          key: recording.fileKey,
          filename: recording.getFileName(),
          mimeType: recording.fileMimeType,
        }

        downloadRawData = null;
        if (recording.canGetRaw()) {
          downloadRawData = {
            _type: 'fileDownload',
            key: recording.rawFileKey,
            filename: "rawData",
            mimeType: null,
          };
        }
        delete recording.rawFileKey;

        return responseUtil.send(response, {
          statusCode: 200,
          success: true,
          messages: [],
          recording: recording,
          downloadFileJWT: jsonwebtoken.sign(
            downloadFileData,
            config.server.passportSecret,
            { expiresIn: 60 * 10 }
          ),
          downloadRawJWT: jsonwebtoken.sign(
            downloadRawData,
            config.server.passportSecret,
            { expiresIn: 60 * 10 }
          ),
        });
      }
    );
};
<commit_msg>Added ability to get recordings are are or aren't tagged.<commit_after>var models = require('../../models');
var util = require('./util');
var passport = require('passport');
var log = require('../../logging');
var tagsUtil = require('./tagsUtil');
var requestUtil = require('./requestUtil');
var responseUtil = require('./responseUtil');
var multiparty = require('multiparty');
var config = require('../../config/config')
var uuidv4 = require('uuid/v4');
var jsonwebtoken = require('jsonwebtoken');
var sequelize = require('sequelize');
var modelsUtil = require('../../models/util/util');

module.exports = (app, baseUrl) => {
  var apiUrl = baseUrl + '/recordings';

  /**
   * @api {post} /api/v1/recordings Add a new recording.
   * @apiName PostRecording
   * @apiGroup Recordings
   * @apiDescription This call is used for uploads of new recordings. It
   * currently supports raw thermal video but will eventually support all
   * recording types. It takes a 'data' field which contains a JSON object
   * string that can contain any of the following fields, Note that 'type'
   * is required:
   * - type: REQUIRED, one of ('thermalRaw')
   * - duration
   * - recordingDateTime
   * - location
   * - version
   * - batteryCharging
   * - batteryLevel
   * - airplaneModeOn
   * - additionalMetadata
   * - processingMeta
   *
   * @apiUse V1DeviceAuthorizationHeader
   *
   * @apiParam {JSON} data Metadata about the recording (see above).
   * @apiParam {File} file File of the recording.
   *
   * @apiUse V1ResponseSuccess
   * @apiuse V1ResponseError
   */
  app.post(
    apiUrl,
    passport.authenticate(['jwt'], { session: false }),
    async (request, response) => {
      log.info(request.method + " Request: " + request.url);

      // Check that the request was authenticated by a device.
      if (!requestUtil.isFromADevice(request))
        return responseUtil.notFromADevice(response);
      var device = request.user;

      var recording;
      var key = uuidv4();
      var form = new multiparty.Form();
      var validData = false;
      var uploadProcess = null;
      var uploadStarted = false;
      var invalidMessages = [];

      // Make new Recording from the data field and Device.
      // TODO Stop stream if 'data' is invalid.
      form.on('field', (name, value) => {
        if (name != 'data') return; // Only parse data field.
        try {
          var data = JSON.parse(value);
          recording = models.Recording.build(data, {
            fields: models.Recording.apiSettableFields,
          });
          recording.set('rawFileKey', key);
          recording.set('DeviceId', device.id);
          recording.set('GroupId', device.GroupId);
          recording.set('processingState',
            models.Recording.processingStates[data.type][0]);
          if (typeof device.public === 'boolean')
            recording.set('public', device.public);
          validData = true;
        } catch (e) {
          log.debug(e);
          // TODO Stop stream here.
        }
      });

      // Stream file to LeoFS.
      form.on('part', (part) => {
        if (part.name != 'file') return part.resume();
        uploadStarted = true;
        log.debug('Streaming file to LeoFS.')
        uploadPromise = new Promise(function(resolve, reject) {
          var s3 = modelsUtil.openS3();
          s3.upload({
            Bucket: config.s3.bucket,
            Key: key,
            Body: part,
          }, (err, data) => {
            if (err) return reject(err);
            log.info("Finished streaming file to LeoFS Key:", key);
            resolve();
          });
        });
      });

      // Close response.
      form.on('close', async () => {
        log.info("Finished POST request.");
        if (!validData || !uploadStarted)
          return responseUtil.invalidDatapointUpload(response);

        // Check that th file uploaded to LeoFS.
        try {
          await uploadPromise;
        } catch (e) {
          return responseUtil.send(response, {
            statusCode: 500,
            success: false,
            messages: ["Failed to upload file to LeoFS"],
          });
        }
        // Validate and upload recording
        await recording.validate();
        await recording.save();
        return responseUtil.validDatapointUpload(response);
      });

      form.on('error', (e) => {
        log.error(e);
      })

      form.parse(request);
    });

  /**
   * @api {get} /api/v1/recordings Query available recordings
   * @apiName GetRecordigns
   * @apiGroup Recordings
   *
   * @apiUse V1UserAuthorizationHeader
   *
   * @apiParam {JSON} where [Sequelize where conditions](http://docs.sequelizejs.com/manual/tutorial/querying.html#where) for query
   * @apiParam {Number} offset Query result offset (for paging).
   * @apiParam {Number} limit Query result limit (for paging).
   * @apiParam {JSON} [order] [Sequelize ordering](http://docs.sequelizejs.com/manual/tutorial/querying.html#ordering). Exampel: [["recordingDateTime", "ASC"]]
   *
   * @apiUse V1ResponseSuccess
   * @apiSuccess {Number} offset Mirrors request offset parameter.
   * @apiSuccess {Number} limit Mirrors request limit parameter.
   * @apiSuccess {Number} count Total number of records which match the query.
   * @apiSuccess {JSON} rows List of details for records which matched the query.
   *
   * @apiUse V1ResponseError
   */
  app.get(
    apiUrl,
    passport.authenticate(['jwt'], { session: false }),
    async (request, response) => {
      log.info(request.method + " Request: " + request.url);

      // Check that the request was authenticated by a User.
      if (!requestUtil.isFromAUser(request))
        return responseUtil.notFromAUser(response);

      var where = request.query.where;
      var offset = parseInt(request.query.offset);
      var limit = parseInt(request.query.limit);
      var order = request.query.order;

      // Validate 'where', 'offset', and 'limit'
      var errorMessages = [];
      try {
        where = JSON.parse(where);
      } catch (e) {
        errorMessages.push("'where' field was not a valid JSON.")
      }
      if (isNaN(offset)) errorMessages.push("'offset' was not a number.")
      if (isNaN(limit)) errorMessages.push("'limit' was not a number.")
      if (order != null) {
        try {
          order = JSON.parse(order);
        } catch (e) {
          errorMessages.push("'order' was not a valid JSON.")
        }
      } else {
        order = [
          // Sort by recordingDatetime but handle the case of the
          // timestamp being missing and fallback to sorting by id.
          [sequelize.fn("COALESCE", sequelize.col('recordingDateTime'), '1970-01-01'), "DESC"],
          ["id", "DESC"],
        ];
      }
      if (errorMessages.length > 0) {
        return responseUtil.send(response, {
          statusCode: 400,
          success: false,
          messages: errorMessages,
        });
      }

      // If query should be filtered by tagged or not or ignored.
      var sqlLiteral = '';
      var tagRequired = false;
      if (where._tagged == true) {
        tagRequired = true;
      } else if (where._tagged == false) {
        sqlLiteral = 'NOT EXISTS (SELECT * FROM   "Tags" WHERE  "Tags".id = "Recording".id)';
        tagRequired = false;
      }
      delete where._tagged;

      // Make sequelize query.
      var userGroupIds = await request.user.getGroupsIds();
      var query = {
        where: {
          "$and": [
            where, // User query
            { "$or": [{ public: true }, { GroupId: { "$in": userGroupIds } }] },
            sequelize.literal(sqlLiteral),
          ],
        },
        order: order,
        include: [
          { model: models.Group },
          { model: models.Tag, required: tagRequired },
          { model: models.Device, where: {}, attributes: ["devicename", "id"] },
        ],
        limit: limit,
        offset: offset,
        attributes: models.Recording.userGetAttributes,
      };
      // Query database.
      var result = await models.Recording.findAndCount(query);

      // Send response
      return responseUtil.send(response, {
        statusCode: 200,
        success: true,
        messages: ["Completed query."],
        limit: limit,
        offset: offset,
        count: result.count,
        rows: result.rows,
      });
    });

    /**
    * @api {get} /api/v1/recordings/:id Get a recording
    * @apiName GetRecording
    * @apiGroup Recordings
    * @apiDescription This call returns metadata for a recording in JSON format
    * and a JSON Web Token (JWT) which can be used to retrieve the recorded
    * content. This is should be used with the
    * [/api/v1/signedUrl API](#api-SignedUrl-GetFile).
    *
    * @apiUse V1UserAuthorizationHeader
    *
    * @apiUse V1ResponseSuccess
    * @apiSuccess {String} downloadFileJWT JSON Web Token to use to download the
    * recording file.
    * @apiSuccess {String} downloadRawJWT JSON Web Token to use to download
    * the raw recording data.
    * @apiSuccess {JSON} recording The recording data.
    *
    * @apiUse V1ResponseError
    */
    app.get(
      apiUrl + '/:id',
      passport.authenticate(['jwt'], { session: false }),
      async (request, response) => {
        log.info(request.method + " Request: " + request.url);

        // Check that the request was authenticated by a User.
        if (!requestUtil.isFromAUser(request))
          return responseUtil.notFromAUser(response);

        var id = parseInt(request.params.id);
        if (!id)
          return responseUtil.invalidDataId(response);

        // Make sequelize query.
        var userGroupIds = await request.user.getGroupsIds();
        var query = {
          where: {
            "$and": [
              { id: id },
              { "$or": [{ public: true }, { GroupId: { "$in": userGroupIds } }] }
            ],
          },
          include: [
            { model: models.Tag, },
            { model: models.Device, where: {}, attributes: ["devicename", "id"] },
          ],
          attributes: models.Recording.userGetAttributes.concat(['rawFileKey']),
        };

        // Query database.
        var recording = await models.Recording.findOne(query);

        downloadFileData = {
          _type: 'fileDownload',
          key: recording.fileKey,
          filename: recording.getFileName(),
          mimeType: recording.fileMimeType,
        }

        downloadRawData = null;
        if (recording.canGetRaw()) {
          downloadRawData = {
            _type: 'fileDownload',
            key: recording.rawFileKey,
            filename: "rawData",
            mimeType: null,
          };
        }
        delete recording.rawFileKey;

        return responseUtil.send(response, {
          statusCode: 200,
          success: true,
          messages: [],
          recording: recording,
          downloadFileJWT: jsonwebtoken.sign(
            downloadFileData,
            config.server.passportSecret,
            { expiresIn: 60 * 10 }
          ),
          downloadRawJWT: jsonwebtoken.sign(
            downloadRawData,
            config.server.passportSecret,
            { expiresIn: 60 * 10 }
          ),
        });
      }
    );
};
<|endoftext|>


<commit_before>// OpenJSCAD port of the OpenSCAD
// Pin Connectors V2 by
// Tony Buser <tbuser@gmail.com>
// see http://www.thingiverse.com/thing:10541
//
// ported by Chris Jackson http://www.thingiverse.com/faithhack/about

// h = shaft height
// r = shaft radius
// lh = lip height
// lt = lip thickness
// t = tolerance
// tight = set to false if you want a joint that spins easily

function main() {

  return pin();

}
function pinhole({h=10, r=4, lh=3, lt=1, t=0.3, tight=true} = {}) {
  var ret = union(
    pin_solid(h, r+(t/2), lh, lt),
    cylinder({h:h+0.2, r:r}),
    // widen the entrance hole to make insertion easier
    cylinder(h=lh/3, r2=r, r1=r+(t/2)+(lt/2)).translate([0, 0, -0.1])
  );
  // widen the cylinder slightly
  // cylinder(h=h+0.2, r=r+(t-0.2/2));

  return ret;
}

// bh = base_height
// br = base_radius
function pintack({h=10, r=4, lh=3, lt=1, bh=3, br=8.75} = {}) {
  return union(
    cylinder({h:bh, r:br}),
    pin(h, r, lh, lt).translate([0, 0, bh])
  );
}

function pinpeg({h=20, r=4, lh=3, lt=1} = {}) {
  return union(
    pin(h/2+0.1, r, lh, lt, side=true).translate([0, -h/4+0.05, 0]),
    rotate([0, 0, 180],pin(h/2+0.1, r, lh, lt, side=true)).translate([0, h/4-0.05, 0])
  );
}

// just call pin instead, I made this module because it was easier to do the rotation option this way
// since openscad complains of recursion if I did it all in one module
// h = shaft height
// r = shaft radius
// lh = lip height
// lt = lip thickness

function pin_vertical(h, r, lh, lt) {
  return difference(
    pin_solid(h, r, lh, lt),
    // center cut
    cube([r*0.5, r*2+lt*2, h]).translate([-r*0.5/2, -(r*2+lt*2)/2, h/4]),
    cylinder({h:h+lh, r:r/2.5, fn:20}).translate([0, 0, h/4]),
    // side cuts
    cube([r*4, lt*2, h+2]).translate([-r*2, -lt-r*1.125, -1]),
    cube([r*4, lt*2, h+2]).translate([-r*2, -lt+r*1.125, -1])
  );
}

// call pin with side=true instead of this
// h = shaft height
// r = shaft radius
// lh = lip height
// lt = lip thickness
function pin_horizontal(h, r, lh, lt) {
  return pin_vertical(h, r, lh, lt).rotateX(90).translate([0, h/2, r*1.125-lt]);
}

// this is mainly to make the pinhole module easier
function pin_solid(h, r, lh, lt) {
  return union(
    // shaft
    cylinder({h:h-lh, r:r, fn:30}),
    // lip
    cylinder({h:lh*0.25, r1:r, r2:r+(lt/2), fn:30}).translate([0, 0, h-lh]),
    cylinder({h:lh*0.25, r:r+(lt/2), fn:30}).translate([0, 0, h-lh+lh*0.25]),
    cylinder({h:lh*0.50, r1:r+(lt/2), r2:r-(lt/2), fn:30}).translate([0, 0, h-lh+lh*0.50])
  );
}
<commit_msg>Archivo borrado<commit_after><|endoftext|>


<commit_before>import React from 'react';
import { Route } from 'react-router';

import AppContainer from '../containers/App';
import LoggedInContainer from '../containers/LoggedIn';
import ItemListContainer from '../containers/ItemList';
import ItemDetailsContainer from '../containers/ItemDetails';
import EntityNotFound from '../components/EntityNotFound';

export default (
  <Route path="/" handler={AppContainer}>
    <Route path="/logged-in" handler={LoggedInContainer} />
    <Route path="/item-list" handler={ItemListContainer} />
    <Route path="/item-details/:itemId" handler={ItemDetailsContainer} />
    <Route path="/entity-not-found" handler={EntityNotFound} />
  </Route>
);
<commit_msg>Updated routes to use `component` prop instead of `handler`<commit_after>import React from 'react';
import { Route } from 'react-router';

import AppContainer from '../containers/App';
import LoggedInContainer from '../containers/LoggedIn';
import ItemListContainer from '../containers/ItemList';
import ItemDetailsContainer from '../containers/ItemDetails';
import EntityNotFound from '../components/EntityNotFound';

export default (
  <Route path="/" component={AppContainer}>
    <Route path="/logged-in" component={LoggedInContainer} />
    <Route path="/item-list" component={ItemListContainer} />
    <Route path="/item-details/:itemId" component={ItemDetailsContainer} />
    <Route path="/entity-not-found" component={EntityNotFound} />
  </Route>
);
<|endoftext|>


<commit_before>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      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.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      contributors: Alejandro Vera (alejandro.vera@centeropenmiddleware.com ),
                    Carlos Blanco. (carlos.blanco@centeropenmiddleware.com)
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

(function() {

    /**
     *
     * @param configuration
     * @returns {*}
     */
    var normalizeConfig = function normalizeConfig(configuration) {
        if (configuration == null) {
            configuration = {};
        }

        var defaultConfig = {
            columns: {
                type: 'object',
                default: []
            },
            height: {
                type: 'number',
                default: 240
            },
            filterControl: {
                type: 'boolean',
                default: false
            },
            paginationControl: {
                type: 'boolean',
                default: false
            },
            lengthControl: {
                type: 'boolean',
                default: false
            },
            tableInfo: {
                type: 'boolean',
                default: false
            },
            selectable: {
                type: 'boolean',
                default: true
            },
            maxRowsSelected: {
                type: 'number',
                default: 1
            },
            updateContexts: {
                type: 'object',
                default: null
            }

        };

        for(var confName in defaultConfig) {
            var conf = defaultConfig[confName];
            if (typeof configuration[confName] != conf['type']) {
                configuration[confName] = conf['default'];
            }
        }

        return configuration;
    };

    /* Table constructor
     *   element: the DOM element that will contain the rangeNv
     *   data: the resources array. It should contain only a resource, or at least the same resources with different
     *      parameters.
     *   contextId: optional.
     *   configuration: additional table configuration:
     *      {
     *       ~ height: number - Height of the widget.
     *       ~ filterControl: boolean - Whether to display the search filter control or not.
     *       ~ paginationControl: boolean - Whether to display the pagination control or not.
     *       ~ lengthControl: boolean - Whether to display a selector with the number of rows to show or not.
     *       ~ tableInfo: boolean - Whether to show extra info about the table or not.
     *       ~ selectable: boolean - If true, the rows of this table can be selected. When selected, the contexts are
     *          are updated with the data updateContexts indicates.
     *       ~ maxRowsSelected: number - Maximum number of rows that can be selected at the same time.
     *       ~ updateContexts: array - Array of objects that configure how to update the contexts. It must contain an id with
     *          the id of the context and a filter array with the data to send through the context update.
     *          Each filter must contain a 'property' property with the name of the property of the data retrieved from
     *          the framework and can optionally add an 'as' property to indicate the name that it must have in the
     *          object that will be sent through the updateContext.
     *          Format:
     *          {
     *               id: <String>,
     *               filter: [
     *                   {
     *                       property: <String>,
     *                       as: <String> //optional
     *                   }
     *               ]
     *          }
     *          Example (listing all the repositories):
     *          {
     *               id: "repoContext",
     *               filter: [
     *                   {
     *                       property: "repositoryid",
     *                       as: "rid"
     *                   }
     *               ]
     *          }
     *       ~ columns: array - Array of objects with the configuration of all the columns to display in the table.
     *          Column object contains a 'label' and a 'property' or a link.
     *          In case of a property, it is the name of the property of the data retrieved from the framework that
     *          must be displayed in that column.
     *          In case of a link, it displays a link to change to other dashboard. A link is an object with multiple
     *          properties: a 'href' with the name of the dashboard to go to, an 'icon' (class of the icon to display)
     *          or a 'label' (text) to display, and an 'env' that configures the environment info to send to the
     *          new dashboard. The 'env' property is an array of objects that contains a 'property' and an optional 'as'.
     *          Format:
     *          {
     *               label: <String>,
     *               property: <String>
     *           },
     *           {
     *              label: "Show",
     *               link: {
     *                   icon: <String>, //or label: <String>
     *                   href: <String>,
     *                   env: [
     *                       {
     *                           property: <String>,
     *                           as: <String>
     *                       }
     *                   ]
     *               }
     *          }, ...
     *          Example (listing all the repositories and changing to the dashboard of the selected repository):
     *          {
     *               label: "Name",
     *               property: "name"
     *           },
     *           {
     *              label: "Go to",
     *               link: {
     *                   icon: "fa fa-share-square-o",
     *                   href: "repository-dashboard",
     *                   env: [
     *                       {
     *                           property: "repositoryid",
     *                           as: "rid"
     *                       }
     *                   ]
     *               }
     *          }
     *      }
     */
    var Table = function Table(element, metrics, contextId, configuration) {

        if(!framework.isReady()) {
            console.error("Table object could not be created because framework is not loaded.");
            return;
        }

        this.element = $(element); //Store as jquery object
        this.data = null;

        // Extending widget
        framework.widgets.CommonWidget.call(this, false, this.element.get(0));

        // Configuration
        this.configuration = normalizeConfig(configuration);

        this.observeCallback = function(event){

            if(event.event === 'loading') {
                this.startLoading();
            } else if(event.event === 'data') {
                this.endLoading(this.updateData.bind(this, event.data));
            }

        }.bind(this);

        framework.data.observe(metrics, this.observeCallback , contextId);

    };

    Table.prototype = new framework.widgets.CommonWidget(true);

    /**
     *
     * @param framework_data
     */
    Table.prototype.updateData = function(framework_data) {

        if(this.tableDom == null) {

            //Create the html for the table
            this.element.append('

');
            this.tableDom = this.element.children("table");
            this.tableDom.get(0).style.minHeight = this.configuration.height;

            //Add click listener for links
            this.tableDom.on( 'click', '.dashboardLink', this, dashboardLinkClickHandler);
        }

        var normalizedData = getNormalizedData.call(this,framework_data);

        var head = this.tableDom.find("thead tr");
        head.empty();

        for(var i in this.configuration.columns) {
            head.append("" + this.configuration.columns[i]['label'] + "	")
        }

        //Create dom configuration string
        var dom = "t";
        if(this.configuration.filterControl) dom += 'f';
        if(this.configuration.lengthControl) dom += 'l';
        if(this.configuration.tableInfo) dom += 'i';
        if(this.configuration.paginationControl) dom += 'p';

        // Get columns
        var columns = [];
        for(var i in this.configuration.columns) {

            var column = this.configuration.columns[i];

            if(column['property'] != null) { //Is a property
                columns.push({data: column['property']});

            } else if(column['link'] != null && column['link']['href'] != null) { //Is a link

                columns.push( {
                    data: function(){return column['link']},
                    orderable: false,
                    searchable: false,
                    render: columnIconRenderer
                } );
            }

        }

        //DataTable object
        this.table = this.tableDom.DataTable({
            data: normalizedData,
            dom: dom,
            columns: columns
        });

        //The rows can be selected
        if(this.configuration.selectable) {
            this.tableDom.on( 'click', 'tbody tr', this, rowClickHandler);
        }

    };

    /**
     * Deletes the widget.
     */
    Table.prototype.delete = function() {

        //Stop observing for data changes
        framework.data.stopObserve(this.observeCallback);

        //Remove event listeners
        this.tableDom.off();

        //Destroy DataTable
        this.table.destroy()

        //Clear DOM
        this.tableDom.empty();
        this.element.empty();

        this.tableDom = null;
        this.table = null;

    };

    // PRIVATE METHODS - - - - - - - - - - - - - - - - - - - - - -

    /**
     * Renderer for columns that contain links.
     * @param linkInfo Info about the link. Must contain an icon property (with the class of the icon to display)
     *           or a label property (with the text to display).
     *           See https://datatables.net/reference/option/columns.render#function
     * @param type See https://datatables.net/reference/option/columns.render#function
     * @param rowData See https://datatables.net/reference/option/columns.render#function
     * @returns {*}
     */
    var columnIconRenderer = function columnIconRenderer(linkInfo, type, rowData) {

        if ( type === 'display' ) {

            if (linkInfo['icon'] != null) { //It is an icon
                return '

';
            } else { //Just a label
                return '
' + linkInfo['label'] + '
';
            }

        }

        return linkInfo;

    };

    /**
     * Function that handles the click in a dashboard link.
     * @param e
     */
    var dashboardLinkClickHandler = function dashboardLinkClickHandler(e) {

        e.stopImmediatePropagation(); // stop the row selection when clicking on an icon

        var widget = e.data;
        var cell = $(this).parent();

        var linkInfo = widget.table.cell(cell).data();
        var rowData = widget.table.cell(cell).row().data();

        //Generate the env
        var env = null;
        if (linkInfo['env'] != null) {
            env = generateEnv(linkInfo['env'], rowData);
        }

        //Change the dashboard
        framework.dashboard.changeTo(linkInfo['href'], env);

    };

    /**
     * Function that handles clicks in rows. When selecting a row it may be necessary to update contexts.
     * @param e
     */
    var rowClickHandler = function rowClickHandler(e) {

        var widget = e.data;

        if ( $(this).hasClass('selected') ) { //It is already selected
            $(this).removeClass('selected');

        } else { //Not selected

            //Select it if the maximum of selected rows has not been achieved
            if(widget.configuration.maxRowsSelected > widget.table.$('tr.selected').length) {
                $(this).addClass('selected');
            }

        }

        // Update contexts
        var contexts = widget.configuration.updateContexts;
        if(contexts != null) {

            for(var i in contexts) {
                var context = contexts[i];

                if(context['id'] != null) {

                    // Data to send through the context
                    var data = {};

                    // Filter the data to send through the context update
                    if(context['filter'] != null) {
                        for(var f = 0; f < context['filter'].length; ++f) {

                            var filter = context['filter'][f];
                            var property = filter['property'];
                            var as = filter['as'] || property;

                            if(data[as] == null) {
                                data[as] = [];
                            }

                            //Selected items
                            var selected = widget.table.$('tr.selected');

                            //Calculate the data to send for all the selected rows
                            for(var s = 0, len = selected.length; s < len; ++s) {

                                var selectedData = widget.table.row(selected[s]).data();
                                var propertyValue = selectedData[property];

                                if(property != null && typeof propertyValue !== 'undefined') {
                                    data[as].push(propertyValue);
                                }

                            }
                        }

                    }

                    //Send the update context
                    framework.data.updateContext(context['id'], data);
                }
            }
        }

    };

    /**
     * Generates an environment object containing all the information of the environment
     * @param envProperty The env configuration property
     * @param rowData Al, the data of the row.
     * @returns {{}}
     */
    var generateEnv = function generateEnv(envProperty, rowData) {

        var env = {};

        for(var e = 0; e < envProperty.length; ++e) {

            var envEntry = envProperty[e];
            var property = envEntry['property'];
            var as = envEntry['as'] || property;

            var propertyValue = rowData[property];

            if(typeof propertyValue !== 'undefined') {
                env[as] = propertyValue;
            }

        }

        return env;
    };

    /**
     * Gets a normalized array of data according to the chart expected input from the data returned by the framework.
     * @param framework_data
     * @returns {Array} Contains objects with 'label' and 'value'.
     */
    var getNormalizedData = function getNormalizedData(framework_data) {

        var values = [];
        var labelVariable = /%\w+%/g; //Regex that matches all the "variables" of the label such as %mid%, %pid%...

        for(var metricId in framework_data) {

            for(var m = 0; m < framework_data[metricId].length; ++m) {

                var metricData = framework_data[metricId][m];

                for(var d = 0; d < metricData.length; ++d) {
                    values.push(metricData[d]);
                }

            }
        }

        return values;

    };

    window.framework.widgets.Table = Table;

})();

<commit_msg>[Widget] [Table] Added width configuration parameter for each column.<commit_after>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      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.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      contributors: Alejandro Vera (alejandro.vera@centeropenmiddleware.com ),
                    Carlos Blanco. (carlos.blanco@centeropenmiddleware.com)
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

(function() {

    /**
     *
     * @param configuration
     * @returns {*}
     */
    var normalizeConfig = function normalizeConfig(configuration) {
        if (configuration == null) {
            configuration = {};
        }

        var defaultConfig = {
            columns: {
                type: 'object',
                default: []
            },
            height: {
                type: 'number',
                default: 240
            },
            filterControl: {
                type: 'boolean',
                default: false
            },
            paginationControl: {
                type: 'boolean',
                default: false
            },
            lengthControl: {
                type: 'boolean',
                default: false
            },
            tableInfo: {
                type: 'boolean',
                default: false
            },
            selectable: {
                type: 'boolean',
                default: true
            },
            maxRowsSelected: {
                type: 'number',
                default: 1
            },
            updateContexts: {
                type: 'object',
                default: null
            }

        };

        for(var confName in defaultConfig) {
            var conf = defaultConfig[confName];
            if (typeof configuration[confName] != conf['type']) {
                configuration[confName] = conf['default'];
            }
        }

        return configuration;
    };

    /* Table constructor
     *   element: the DOM element that will contain the rangeNv
     *   data: the resources array. It should contain only a resource, or at least the same resources with different
     *      parameters.
     *   contextId: optional.
     *   configuration: additional table configuration:
     *      {
     *       ~ height: number - Height of the widget.
     *       ~ filterControl: boolean - Whether to display the search filter control or not.
     *       ~ paginationControl: boolean - Whether to display the pagination control or not.
     *       ~ lengthControl: boolean - Whether to display a selector with the number of rows to show or not.
     *       ~ tableInfo: boolean - Whether to show extra info about the table or not.
     *       ~ selectable: boolean - If true, the rows of this table can be selected. When selected, the contexts are
     *          are updated with the data updateContexts indicates.
     *       ~ maxRowsSelected: number - Maximum number of rows that can be selected at the same time.
     *       ~ updateContexts: array - Array of objects that configure how to update the contexts. It must contain an id with
     *          the id of the context and a filter array with the data to send through the context update.
     *          Each filter must contain a 'property' property with the name of the property of the data retrieved from
     *          the framework and can optionally add an 'as' property to indicate the name that it must have in the
     *          object that will be sent through the updateContext.
     *          Format:
     *          {
     *               id: <String>,
     *               filter: [
     *                   {
     *                       property: <String>,
     *                       as: <String> //optional
     *                   }
     *               ]
     *          }
     *          Example (listing all the repositories):
     *          {
     *               id: "repoContext",
     *               filter: [
     *                   {
     *                       property: "repositoryid",
     *                       as: "rid"
     *                   }
     *               ]
     *          }
     *       ~ columns: array - Array of objects with the configuration of all the columns to display in the table.
     *          Column object contains a 'label' and a 'property' or a link. Additionally it can contain a 'width' that
     *          may take any CSS value (3em, 20px, 10%, etc). Please note that pixel perfect column width is virtually
     *          impossible to achieve in tables with dynamic content.
     *          In case of a property, it is the name of the property of the data retrieved from the framework that
     *          must be displayed in that column.
     *          In case of a link, it displays a link to change to other dashboard. A link is an object with multiple
     *          properties: a 'href' with the name of the dashboard to go to, an 'icon' (class of the icon to display)
     *          or a 'label' (text) to display, and an 'env' that configures the environment info to send to the
     *          new dashboard. The 'env' property is an array of objects that contains a 'property' and an optional 'as'.
     *          Format:
     *          {
     *               label: <String>,
     *               property: <String>
     *           },
     *           {
     *              label: "Show",
     *               link: {
     *                   icon: <String>, //or label: <String>
     *                   href: <String>,
     *                   env: [
     *                       {
     *                           property: <String>,
     *                           as: <String>
     *                       }
     *                   ]
     *               }
     *          }, ...
     *          Example (listing all the repositories and changing to the dashboard of the selected repository):
     *          {
     *               label: "Name",
     *               property: "name"
     *           },
     *           {
     *              label: "Go to",
     *               link: {
     *                   icon: "fa fa-share-square-o",
     *                   href: "repository-dashboard",
     *                   env: [
     *                       {
     *                           property: "repositoryid",
     *                           as: "rid"
     *                       }
     *                   ]
     *               }
     *          }
     *      }
     */
    var Table = function Table(element, metrics, contextId, configuration) {

        if(!framework.isReady()) {
            console.error("Table object could not be created because framework is not loaded.");
            return;
        }

        this.element = $(element); //Store as jquery object
        this.data = null;

        // Extending widget
        framework.widgets.CommonWidget.call(this, false, this.element.get(0));

        // Configuration
        this.configuration = normalizeConfig(configuration);

        this.observeCallback = function(event){

            if(event.event === 'loading') {
                this.startLoading();
            } else if(event.event === 'data') {
                this.endLoading(this.updateData.bind(this, event.data));
            }

        }.bind(this);

        framework.data.observe(metrics, this.observeCallback , contextId);

    };

    Table.prototype = new framework.widgets.CommonWidget(true);

    /**
     *
     * @param framework_data
     */
    Table.prototype.updateData = function(framework_data) {

        if(this.tableDom == null) {

            //Create the html for the table
            this.element.append('

');
            this.tableDom = this.element.children("table");
            this.tableDom.get(0).style.minHeight = this.configuration.height;

            //Add click listener for links
            this.tableDom.on( 'click', '.dashboardLink', this, dashboardLinkClickHandler);
        }

        var normalizedData = getNormalizedData.call(this,framework_data);

        var head = this.tableDom.find("thead tr");
        head.empty();

        for(var i in this.configuration.columns) {
            head.append("" + this.configuration.columns[i]['label'] + "	")
        }

        //Create dom configuration string
        var dom = "t";
        if(this.configuration.filterControl) dom += 'f';
        if(this.configuration.lengthControl) dom += 'l';
        if(this.configuration.tableInfo) dom += 'i';
        if(this.configuration.paginationControl) dom += 'p';

        // Get columns
        var columns = [];
        for(var i in this.configuration.columns) {

            var columnConfig = this.configuration.columns[i];
            var column = null;

            if(columnConfig['property'] != null) { //Is a property
                column = { data: columnConfig['property'] };

            } else if(columnConfig['link'] != null && columnConfig['link']['href'] != null) { //Is a link
                column = {
                    data: function(){ return columnConfig['link']; },
                    orderable: false,
                    searchable: false,
                    render: columnIconRenderer
                };

            }

            //If it has the required properties
            if(column != null) {

                if(columnConfig['width'] != null) {
                    column['width'] = columnConfig['width'];
                }

                columns.push(column);
            }

        }

        //DataTable object
        this.table = this.tableDom.DataTable({
            data: normalizedData,
            dom: dom,
            columns: columns
        });

        //The rows can be selected
        if(this.configuration.selectable) {
            this.tableDom.on( 'click', 'tbody tr', this, rowClickHandler);
        }

    };

    /**
     * Deletes the widget.
     */
    Table.prototype.delete = function() {

        //Stop observing for data changes
        framework.data.stopObserve(this.observeCallback);

        //Remove event listeners
        this.tableDom.off();

        //Destroy DataTable
        this.table.destroy()

        //Clear DOM
        this.tableDom.empty();
        this.element.empty();

        this.tableDom = null;
        this.table = null;

    };

    // PRIVATE METHODS - - - - - - - - - - - - - - - - - - - - - -

    /**
     * Renderer for columns that contain links.
     * @param linkInfo Info about the link. Must contain an icon property (with the class of the icon to display)
     *           or a label property (with the text to display).
     *           See https://datatables.net/reference/option/columns.render#function
     * @param type See https://datatables.net/reference/option/columns.render#function
     * @param rowData See https://datatables.net/reference/option/columns.render#function
     * @returns {*}
     */
    var columnIconRenderer = function columnIconRenderer(linkInfo, type, rowData) {

        if ( type === 'display' ) {

            if (linkInfo['icon'] != null) { //It is an icon
                return '

';
            } else { //Just a label
                return '
' + linkInfo['label'] + '
';
            }

        }

        return linkInfo;

    };

    /**
     * Function that handles the click in a dashboard link.
     * @param e
     */
    var dashboardLinkClickHandler = function dashboardLinkClickHandler(e) {

        e.stopImmediatePropagation(); // stop the row selection when clicking on an icon

        var widget = e.data;
        var cell = $(this).parent();

        var linkInfo = widget.table.cell(cell).data();
        var rowData = widget.table.cell(cell).row().data();

        //Generate the env
        var env = null;
        if (linkInfo['env'] != null) {
            env = generateEnv(linkInfo['env'], rowData);
        }

        //Change the dashboard
        framework.dashboard.changeTo(linkInfo['href'], env);

    };

    /**
     * Function that handles clicks in rows. When selecting a row it may be necessary to update contexts.
     * @param e
     */
    var rowClickHandler = function rowClickHandler(e) {

        var widget = e.data;

        if ( $(this).hasClass('selected') ) { //It is already selected
            $(this).removeClass('selected');

        } else { //Not selected

            //Select it if the maximum of selected rows has not been achieved
            if(widget.configuration.maxRowsSelected > widget.table.$('tr.selected').length) {
                $(this).addClass('selected');
            }

        }

        // Update contexts
        var contexts = widget.configuration.updateContexts;
        if(contexts != null) {

            for(var i in contexts) {
                var context = contexts[i];

                if(context['id'] != null) {

                    // Data to send through the context
                    var data = {};

                    // Filter the data to send through the context update
                    if(context['filter'] != null) {
                        for(var f = 0; f < context['filter'].length; ++f) {

                            var filter = context['filter'][f];
                            var property = filter['property'];
                            var as = filter['as'] || property;

                            if(data[as] == null) {
                                data[as] = [];
                            }

                            //Selected items
                            var selected = widget.table.$('tr.selected');

                            //Calculate the data to send for all the selected rows
                            for(var s = 0, len = selected.length; s < len; ++s) {

                                var selectedData = widget.table.row(selected[s]).data();
                                var propertyValue = selectedData[property];

                                if(property != null && typeof propertyValue !== 'undefined') {
                                    data[as].push(propertyValue);
                                }

                            }
                        }

                    }

                    //Send the update context
                    framework.data.updateContext(context['id'], data);
                }
            }
        }

    };

    /**
     * Generates an environment object containing all the information of the environment
     * @param envProperty The env configuration property
     * @param rowData Al, the data of the row.
     * @returns {{}}
     */
    var generateEnv = function generateEnv(envProperty, rowData) {

        var env = {};

        for(var e = 0; e < envProperty.length; ++e) {

            var envEntry = envProperty[e];
            var property = envEntry['property'];
            var as = envEntry['as'] || property;

            var propertyValue = rowData[property];

            if(typeof propertyValue !== 'undefined') {
                env[as] = propertyValue;
            }

        }

        return env;
    };

    /**
     * Gets a normalized array of data according to the chart expected input from the data returned by the framework.
     * @param framework_data
     * @returns {Array} Contains objects with 'label' and 'value'.
     */
    var getNormalizedData = function getNormalizedData(framework_data) {

        var values = [];
        var labelVariable = /%\w+%/g; //Regex that matches all the "variables" of the label such as %mid%, %pid%...

        for(var metricId in framework_data) {

            for(var m = 0; m < framework_data[metricId].length; ++m) {

                var metricData = framework_data[metricId][m];

                for(var d = 0; d < metricData.length; ++d) {
                    values.push(metricData[d]);
                }

            }
        }

        return values;

    };

    window.framework.widgets.Table = Table;

})();

<|endoftext|>