<commit_before>'use strict'

const _ = require('lodash')

const Logger = require('./util/logger')
const logger = Logger.global.chain('[jirascope]')

function populateFakeEpicLinks (epicIssue, childIssues) {
  childIssues.forEach((childIssue) => {
    epicIssue.links.push({
      direction: 'inward',
      dstKey: childIssue.key,
      dstStatus: childIssue.status,
      dstStatusCategory: childIssue.statusCategory,
      label: 'is delivered by',
      srcKey: epicIssue.key,
      type: 'Epic'
    })
    childIssue.links.push({
      direction: 'outward',
      dstKey: epicIssue.key,
      dstStatus: epicIssue.status,
      dstStatusCategory: epicIssue.statusCategory,
      label: 'delivers',
      srcKey: childIssue.key,
      type: 'Epic'
    })
  })
}

function processNewIssues (issuesByKey, newIssues) {
  let unvisitedIssues = {}
  let epicIssues = {}
  newIssues.forEach((newIssue) => {
    logger.trace(`${newIssue.key} - new issue found`)

    // filter out issues if they have the wrong prefix
    if (!helpers.isAllowedProjectKey(newIssue.key, this.options.allowedProjectKeys)) {
      logger.trace(`${newIssue.key} - not in allowed project keys - skipping`)
      return
    }

    // filter out links if not in the desired link types
    newIssue.links = newIssue.links.filter((link) => {
      return this.options.followLinkTypes.includes(link.type)
    })

    // filter out links if both nodes are not in the desired status categories
    newIssue.links = newIssue.links.filter((link) => {
      return this.options.followStatusCategories.includes(newIssue.statusCategory) ||
          this.options.followStatusCategories.includes(link.dstStatusCategory)
    })

    // filter out issues if they only have outward links and are not in the desired status categories
    if (_.every(newIssue.links, {direction: 'outward'}) &&
        !this.options.followStatusCategories.includes(newIssue.statusCategory)) {
      logger.trace(`${newIssue.key} - not in desired status categories with only outward links - skipping`)
      return
    }

    issuesByKey[newIssue.key] = newIssue // lets keep it!

    if (newIssue.links.length === 0) {
      logger.trace(`${newIssue.key} - no significant links`)
    } else {
      logger.trace(`${newIssue.key} - evaluating links`)
      newIssue.links.forEach((link) => {
        if (!issuesByKey[link.dstKey]) {
          unvisitedIssues[link.dstKey] = link.dstKey
          logger.trace(`${link.srcKey} ${link.label} ${link.dstKey} - ${link.dstKey} not yet fetched`)
        } else {
          logger.trace(`${link.srcKey} ${link.label} ${link.dstKey} - ${link.dstKey} already fetched`)
        }
      })
    }

    if (newIssue.epicKey && !issuesByKey[newIssue.epicKey]) {
      unvisitedIssues[newIssue.epicKey] = newIssue.epicKey
      logger.trace(`${newIssue.key} - ${newIssue.epicKey} epic not yet fetched`)
    }

    if (newIssue.type === 'Epic') {
      epicIssues[newIssue.key] = newIssue
      logger.trace(`${newIssue.key} - is an epic`)
    }
  })
  const unvisitedIssueKeys = Object.keys(unvisitedIssues)
  return {unvisitedIssueKeys, epicIssues}
}

async function fetchNewIssues (unvisitedIssueKeys, epicIssues) {
  let newIssues = []
  if (unvisitedIssueKeys.length > 0) {
    logger.info(`fetching linked issues`)
    newIssues = newIssues.concat(await this.jiraClient.getIssuesByKey(unvisitedIssueKeys))
  }
  for (let epicIssueKey in epicIssues) {
    logger.info(`${epicIssueKey} - fetching epic child issues`)
    const childIssues = await this.jiraClient.getIssuesByQuery(`"Epic Link" = ${epicIssueKey}`, 1000)
    logger.trace(`${epicIssueKey} - found ${Object.keys(childIssues).length} child issues`)
    newIssues = newIssues.concat(childIssues)
    populateFakeEpicLinks(epicIssues[epicIssueKey], childIssues)
  }
  return newIssues
}

function determineGraphs () {
  const graphs = []
  const issues = _.keyBy(this.issues, 'key')
  while (Object.keys(issues).length !== 0) {
    logger.trace(`issues left to consider: ${Object.keys(issues).length}`)
    const stack = Object.keys(issues).splice(0, 1) // seed the stack
    if (stack.length !== 0) {
      const graph = {
        nodes: [],
        edges: []
      }
      logger.trace(`starting a new graph with ${stack}`)
      while (stack.length !== 0) {
        const key = stack.pop()
        const issue = issues[key]
        logger.trace(`${key}: considering`)
        if (issue) {
          const node = _.omit(issue, ['links'])
          graph.nodes.push(node)
          delete issues[key]
          node.analysis = issue.analysis = {
            orphan: true,
            root: true,
            leaf: true,
            warnings: []
          }

          issue.links.forEach((link) => {
            stack.unshift(link.dstKey) // follow both directions
            if (link.direction === 'inward') {
              issue.analysis.leaf = false
              issue.analysis.orphan = false
              const edge = _.pick(link, ['srcKey', 'dstKey', 'label'])
              graph.edges.push(edge) // but only add the inward to the graph edges to make directed

              if (issue.statusCategory === 'Done' && link.dstStatusCategory !== 'Done') {
                issue.analysis.warnings.push('doneButBlocked')
              }
            }
            if (link.direction === 'outward') {
              issue.analysis.root = false
              issue.analysis.orphan = false
            }
          })

          if (issue.analysis.orphan && !this.options.allowedRootIssueTypes.includes(issue.type)) {
            issue.analysis.warnings.push('orphaned')
            issue.analysis.root = false
            issue.analysis.leaf = false
          }

          if (issue.analysis.root && !this.options.allowedRootIssueTypes.includes(issue.type)) {
            issue.analysis.warnings.push('invalidRoot')
          }
        } else {
          logger.trace(`${key}: already visited`)
        }
      }
      if (graph.nodes.length > 1) {
        graphs.push(graph)
      }
    }
  }
  return graphs
}

function analyseGraph (graph) {
  graph.analysis = {
    size: graph.nodes.length,
    acyclic: true
  }
  graph.label = _.chain(graph.nodes).filter('analysis.root').sortBy(helpers.bySortableKey).map('key').value().join('|')
  graph.nodes = _.orderBy(graph.nodes, helpers.bySortableKey)
  graph.edges = _.orderBy(graph.edges, helpers.bySortableLinkKeys)

  toposort(graph)

  if (graph.analysis.acyclic) {
    score(graph)
  }
}

function toposort (graph) {
  const edges = _.groupBy(graph.edges, 'srcKey')
  const unmarkedNodes = _.keyBy(graph.nodes, 'key')
  const temporaryMarkedNodes = {}
  const sortedNodes = []

  const visit = (key) => {
    logger.trace(`${key} - visiting`)
    if (temporaryMarkedNodes[key]) {
      graph.analysis.acyclic = false
      logger.trace(`${key} - cycle detected!!!`)
      return false
    }
    const node = unmarkedNodes[key]
    if (node) {
      logger.trace(`${key} - marking node`)
      temporaryMarkedNodes[node.key] = node
      // iterate over links
      const acyclic = _.every(edges[node.key] || [], (edge) => {
        logger.trace(`${edge.srcKey} ${edge.label} ${edge.dstKey} - reviewing`)
        return visit(edge.dstKey)
      })
      if (!acyclic) {
        return false
      }

      delete unmarkedNodes[node.key]
      delete temporaryMarkedNodes[node.key]
      sortedNodes.unshift(node)
      logger.trace(`${key} - sorted node - [${_.map(sortedNodes, 'key').join(',')}]`)
    }
    return true
  }

  while (Object.keys(unmarkedNodes).length > 0 && graph.analysis.acyclic) {
    logger.trace(`${Object.keys(unmarkedNodes).length} - unhandled nodes`)
    visit(Object.keys(unmarkedNodes)[0])
  }

  graph.nodes = sortedNodes
}

const priorityScore = {
  'Highest': 5,
  'High': 3,
  'Medium': 1,
  'Low': 0,
  'Lowest': 0
}

function score (graph) {
  const edgesBySrcKey = _.groupBy(graph.edges, 'srcKey')
  const edgesByDstKey = _.groupBy(graph.edges, 'dstKey')
  const nodesByKey = _.keyBy(graph.nodes, 'key')
  const descendents = {}
  const ancestors = {}

  const lookupNode = (key) => { return nodesByKey[key] }

  const size = (key) => {
    const node = nodesByKey[key]
    node.analysis.score = node.analysis.score || priorityScore[node.priority]
  }

  const downstreamScore = (key) => {
    const node = nodesByKey[key]
    if (!descendents[key]) {
      descendents[key] = []
      if (!node.analysis.leaf) {
        edgesBySrcKey[key].forEach((edge) => {
          downstreamScore(edge.dstKey)
          descendents[key] = _.union(descendents[key], [edge.dstKey], descendents[edge.dstKey])
        })
      }
    }
    node.analysis.downstreamScore = _.sumBy(_.map(descendents[key], lookupNode), 'analysis.score') + node.analysis.score
  }

  const upstreamScore = (key) => {
    const node = nodesByKey[key]
    if (!ancestors[key]) {
      ancestors[key] = []
      if (!node.analysis.root) {
        edgesByDstKey[key].forEach((edge) => {
          upstreamScore(edge.srcKey)
          ancestors[key] = _.union(ancestors[key], [edge.srcKey], ancestors[edge.srcKey])
        })
      }
    }
    node.analysis.upstreamScore = _.sumBy(_.map(ancestors[key], lookupNode), 'analysis.score') + node.analysis.score
  }

  const totalScore = (key) => {
    const node = nodesByKey[key]
    const leafDescendentsScore = _.chain(descendents[key]).map(lookupNode).filter('analysis.leaf').sumBy('analysis.upstreamScore').value()
    const rootAncestorsScore = _.chain(ancestors[key]).map(lookupNode).filter('analysis.root').sumBy('analysis.downstreamScore').value()

    node.analysis.totalScore = leafDescendentsScore + rootAncestorsScore
  }

  _.forEach(graph.nodes, (node) => { size(node.key) })
  _.filter(graph.nodes, 'analysis.root').forEach((root) => { downstreamScore(root.key) })
  _.filter(graph.nodes, 'analysis.leaf').forEach((leaf) => { upstreamScore(leaf.key) })
  _.forEach(graph.nodes, (node) => { totalScore(node.key) })
}

/**
 * Provides an understanding of jira issues and their network.
 *
 * @param {Object} jiraClient - The client to use to talk to JIRA
 * @param {Object?} dataStore - The data store to use
 * @param {Object} options - Options to customise behaviour
 * @param {string} options.query - The initial query to use
 * @param {string[]} options.followStatusCategories - An array of status categories to follow
 * @param {string[]} options.followLinkTypes - An array of link types to follow
 * @param {string[]} options.allowedRootIssueTypes - An array of types that are allowed to mark valid root issues
 * @param {string[]} options.allowedProjectKeys - An array of project keys that are allowed, or empty for any
 *
 * @property {Object[]} issues - all significant issues
 * @property {Object[]} graphs - array of distinct graphs containing 'nodes' and 'edges'
 * @property {Object[]} graphs.nodes - array of nodes in the graph, each element being an issue
 * @property {Object[]} graphs.edges - array of edges in the graph, each element being a link
 */
class Jirascope {
  constructor (jiraClient, dataStore, options) {
    this.jiraClient = jiraClient
    this.dataStore = dataStore
    this.options = options
    this.issues = []
    this.graphs = []
  }

  /**
   * Either reads or fetches issues, tidies, and analyses
   */
  async populate () {
    if (this.dataStore) {
      this.issues = await this.dataStore.readData('issues')
    }
    if (!this.issues) {
      await this.fetch()
    }
    this.tidy()
    this.analyse()
  }

  /**
   * Fetches issues from jira
   */
  async fetch () {
    const issuesByKey = {}
    logger.info(`fetching initial issues`)
    let newIssues = await this.jiraClient.getIssuesByQuery(this.options.query, 1000)
    newIssues.forEach((newIssue) => { issuesByKey[newIssue.key] = newIssue }) // initial issues we always keep

    while (newIssues.length > 0) {
      const { unvisitedIssueKeys, epicIssues } = processNewIssues.call(this, issuesByKey, newIssues)
      newIssues = await fetchNewIssues.call(this, unvisitedIssueKeys, epicIssues)
    }
    this.issues = Object.values(issuesByKey)
  }

  /**
   * Tidies up issues, such as pruning partial links and sorting by key numeric part
   */
  tidy () {
    const allIssuesByKey = _.keyBy(this.issues, 'key')
    // prune any partial links now we have fetched everything
    _.forEach(this.issues, (issue) => {
      issue.links = _.filter(issue.links, (link) => {
        return allIssuesByKey[link.srcKey] && allIssuesByKey[link.dstKey] // only include edges that we have issues for
      })
      issue.links = _.orderBy(issue.links, helpers.bySortableLinkKeys)
    })
    this.issues = _.orderBy(this.issues, helpers.bySortableKey)
  }

  /**
   * Analyses issues previously read or fetched and tidied
   */
  analyse () {
    const graphs = determineGraphs.call(this)
    graphs.forEach((graph) => { analyseGraph.call(this, graph) })
    this.graphs = _.orderBy(graphs, ['analysis.size'], ['desc'])
  }

  async store () {
    if (this.dataStore) {
      await this.dataStore.writeData('issues', this.issues)
      await this.dataStore.writeData('graphs', this.graphs)
    }
  }

  async cleanup () {
    this.issues = {}
    if (this.dataStore) {
      await this.dataStore.deleteData('issues')
      await this.dataStore.deleteData('graphs')
    }
  }

  get warningTypes () {
    return warningTypes
  }

  get helpers () {
    return helpers
  }
}

const warningTypes = {
  doneButBlocked: {
    label: 'Done But Blocked'
  },
  invalidRoot: {
    label: 'Invalid Root'
  },
  orphaned: {
    label: 'Orphaned'
  }
}

const helpers = {
  sortableKey: (key) => { return [_.split(key, '-')[0], _.padStart(_.split(key, '-')[1], 10, '0')].join('-') },
  bySortableKey: (x) => { return helpers.sortableKey(x.key) },
  bySortableLinkKeys: (x) => { return `${helpers.sortableKey(x.srcKey)}-${helpers.sortableKey(x.dstKey)}` },
  hasWarnings: (x) => { return x.analysis.warnings.length > 0 },
  isAllowedProjectKey: (key, allowedProjectKeys) => {
    if (allowedProjectKeys.length === 0) {
      return true
    }
    return _.some(allowedProjectKeys, (allowedProjectKey) => { return _.startsWith(key, allowedProjectKey) })
  }
}

Jirascope.helpers = helpers
Jirascope.warningTypes = warningTypes

exports = module.exports = Jirascope
<commit_msg>tweak scoring<commit_after>'use strict'

const _ = require('lodash')

const Logger = require('./util/logger')
const logger = Logger.global.chain('[jirascope]')

function populateFakeEpicLinks (epicIssue, childIssues) {
  childIssues.forEach((childIssue) => {
    epicIssue.links.push({
      direction: 'inward',
      dstKey: childIssue.key,
      dstStatus: childIssue.status,
      dstStatusCategory: childIssue.statusCategory,
      label: 'is delivered by',
      srcKey: epicIssue.key,
      type: 'Epic'
    })
    childIssue.links.push({
      direction: 'outward',
      dstKey: epicIssue.key,
      dstStatus: epicIssue.status,
      dstStatusCategory: epicIssue.statusCategory,
      label: 'delivers',
      srcKey: childIssue.key,
      type: 'Epic'
    })
  })
}

function processNewIssues (issuesByKey, newIssues) {
  let unvisitedIssues = {}
  let epicIssues = {}
  newIssues.forEach((newIssue) => {
    logger.trace(`${newIssue.key} - new issue found`)

    // filter out issues if they have the wrong prefix
    if (!helpers.isAllowedProjectKey(newIssue.key, this.options.allowedProjectKeys)) {
      logger.trace(`${newIssue.key} - not in allowed project keys - skipping`)
      return
    }

    // filter out links if not in the desired link types
    newIssue.links = newIssue.links.filter((link) => {
      return this.options.followLinkTypes.includes(link.type)
    })

    // filter out links if both nodes are not in the desired status categories
    newIssue.links = newIssue.links.filter((link) => {
      return this.options.followStatusCategories.includes(newIssue.statusCategory) ||
          this.options.followStatusCategories.includes(link.dstStatusCategory)
    })

    // filter out issues if they only have outward links and are not in the desired status categories
    if (_.every(newIssue.links, {direction: 'outward'}) &&
        !this.options.followStatusCategories.includes(newIssue.statusCategory)) {
      logger.trace(`${newIssue.key} - not in desired status categories with only outward links - skipping`)
      return
    }

    issuesByKey[newIssue.key] = newIssue // lets keep it!

    if (newIssue.links.length === 0) {
      logger.trace(`${newIssue.key} - no significant links`)
    } else {
      logger.trace(`${newIssue.key} - evaluating links`)
      newIssue.links.forEach((link) => {
        if (!issuesByKey[link.dstKey]) {
          unvisitedIssues[link.dstKey] = link.dstKey
          logger.trace(`${link.srcKey} ${link.label} ${link.dstKey} - ${link.dstKey} not yet fetched`)
        } else {
          logger.trace(`${link.srcKey} ${link.label} ${link.dstKey} - ${link.dstKey} already fetched`)
        }
      })
    }

    if (newIssue.epicKey && !issuesByKey[newIssue.epicKey]) {
      unvisitedIssues[newIssue.epicKey] = newIssue.epicKey
      logger.trace(`${newIssue.key} - ${newIssue.epicKey} epic not yet fetched`)
    }

    if (newIssue.type === 'Epic') {
      epicIssues[newIssue.key] = newIssue
      logger.trace(`${newIssue.key} - is an epic`)
    }
  })
  const unvisitedIssueKeys = Object.keys(unvisitedIssues)
  return {unvisitedIssueKeys, epicIssues}
}

async function fetchNewIssues (unvisitedIssueKeys, epicIssues) {
  let newIssues = []
  if (unvisitedIssueKeys.length > 0) {
    logger.info(`fetching linked issues`)
    newIssues = newIssues.concat(await this.jiraClient.getIssuesByKey(unvisitedIssueKeys))
  }
  for (let epicIssueKey in epicIssues) {
    logger.info(`${epicIssueKey} - fetching epic child issues`)
    const childIssues = await this.jiraClient.getIssuesByQuery(`"Epic Link" = ${epicIssueKey}`, 1000)
    logger.trace(`${epicIssueKey} - found ${Object.keys(childIssues).length} child issues`)
    newIssues = newIssues.concat(childIssues)
    populateFakeEpicLinks(epicIssues[epicIssueKey], childIssues)
  }
  return newIssues
}

function determineGraphs () {
  const graphs = []
  const issues = _.keyBy(this.issues, 'key')
  while (Object.keys(issues).length !== 0) {
    logger.trace(`issues left to consider: ${Object.keys(issues).length}`)
    const stack = Object.keys(issues).splice(0, 1) // seed the stack
    if (stack.length !== 0) {
      const graph = {
        nodes: [],
        edges: []
      }
      logger.trace(`starting a new graph with ${stack}`)
      while (stack.length !== 0) {
        const key = stack.pop()
        const issue = issues[key]
        logger.trace(`${key}: considering`)
        if (issue) {
          const node = _.omit(issue, ['links'])
          graph.nodes.push(node)
          delete issues[key]
          node.analysis = issue.analysis = {
            orphan: true,
            root: true,
            leaf: true,
            warnings: []
          }

          issue.links.forEach((link) => {
            stack.unshift(link.dstKey) // follow both directions
            if (link.direction === 'inward') {
              issue.analysis.leaf = false
              issue.analysis.orphan = false
              const edge = _.pick(link, ['srcKey', 'dstKey', 'label'])
              graph.edges.push(edge) // but only add the inward to the graph edges to make directed

              if (issue.statusCategory === 'Done' && link.dstStatusCategory !== 'Done') {
                issue.analysis.warnings.push('doneButBlocked')
              }
            }
            if (link.direction === 'outward') {
              issue.analysis.root = false
              issue.analysis.orphan = false
            }
          })

          if (issue.analysis.orphan && !this.options.allowedRootIssueTypes.includes(issue.type)) {
            issue.analysis.warnings.push('orphaned')
            issue.analysis.root = false
            issue.analysis.leaf = false
          }

          if (issue.analysis.root && !this.options.allowedRootIssueTypes.includes(issue.type)) {
            issue.analysis.warnings.push('invalidRoot')
          }
        } else {
          logger.trace(`${key}: already visited`)
        }
      }
      if (graph.nodes.length > 1) {
        graphs.push(graph)
      }
    }
  }
  return graphs
}

function analyseGraph (graph) {
  graph.analysis = {
    size: graph.nodes.length,
    acyclic: true
  }
  graph.label = _.chain(graph.nodes).filter('analysis.root').sortBy(helpers.bySortableKey).map('key').value().join('|')
  graph.nodes = _.orderBy(graph.nodes, helpers.bySortableKey)
  graph.edges = _.orderBy(graph.edges, helpers.bySortableLinkKeys)

  toposort(graph)

  if (graph.analysis.acyclic) {
    score(graph)
  }
}

function toposort (graph) {
  const edges = _.groupBy(graph.edges, 'srcKey')
  const unmarkedNodes = _.keyBy(graph.nodes, 'key')
  const temporaryMarkedNodes = {}
  const sortedNodes = []

  const visit = (key) => {
    logger.trace(`${key} - visiting`)
    if (temporaryMarkedNodes[key]) {
      graph.analysis.acyclic = false
      logger.trace(`${key} - cycle detected!!!`)
      return false
    }
    const node = unmarkedNodes[key]
    if (node) {
      logger.trace(`${key} - marking node`)
      temporaryMarkedNodes[node.key] = node
      // iterate over links
      const acyclic = _.every(edges[node.key] || [], (edge) => {
        logger.trace(`${edge.srcKey} ${edge.label} ${edge.dstKey} - reviewing`)
        return visit(edge.dstKey)
      })
      if (!acyclic) {
        return false
      }

      delete unmarkedNodes[node.key]
      delete temporaryMarkedNodes[node.key]
      sortedNodes.unshift(node)
      logger.trace(`${key} - sorted node - [${_.map(sortedNodes, 'key').join(',')}]`)
    }
    return true
  }

  while (Object.keys(unmarkedNodes).length > 0 && graph.analysis.acyclic) {
    logger.trace(`${Object.keys(unmarkedNodes).length} - unhandled nodes`)
    visit(Object.keys(unmarkedNodes)[0])
  }

  graph.nodes = sortedNodes
}

const priorityScore = {
  'Highest': 5,
  'High': 3,
  'Medium': 1,
  'Low': 0,
  'Lowest': 0
}

function score (graph) {
  const edgesBySrcKey = _.groupBy(graph.edges, 'srcKey')
  const edgesByDstKey = _.groupBy(graph.edges, 'dstKey')
  const nodesByKey = _.keyBy(graph.nodes, 'key')
  const descendents = {}
  const ancestors = {}

  const lookupNode = (key) => { return nodesByKey[key] }

  const size = (key) => {
    const node = nodesByKey[key]
    node.analysis.score = node.analysis.score || priorityScore[node.priority]
  }

  const downstreamScore = (key) => {
    const node = nodesByKey[key]
    if (!descendents[key]) {
      descendents[key] = []
      if (!node.analysis.leaf) {
        edgesBySrcKey[key].forEach((edge) => {
          downstreamScore(edge.dstKey)
          descendents[key] = _.union(descendents[key], [edge.dstKey], descendents[edge.dstKey])
        })
      }
    }
    node.analysis.downstreamScore = _.sumBy(_.map(descendents[key], lookupNode), 'analysis.score') + node.analysis.score
  }

  const upstreamScore = (key) => {
    const node = nodesByKey[key]
    if (!ancestors[key]) {
      ancestors[key] = []
      if (!node.analysis.root) {
        edgesByDstKey[key].forEach((edge) => {
          upstreamScore(edge.srcKey)
          ancestors[key] = _.union(ancestors[key], [edge.srcKey], ancestors[edge.srcKey])
        })
      }
    }
    node.analysis.upstreamScore = _.sumBy(_.map(ancestors[key], lookupNode), 'analysis.score') + node.analysis.score
  }

  const totalScore = (key) => {
    const node = nodesByKey[key]
    const leafDescendentsScore = _.chain(descendents[key]).map(lookupNode).filter('analysis.leaf').sumBy('analysis.upstreamScore').value()
    const rootAncestorsScore = _.chain(ancestors[key]).map(lookupNode).filter('analysis.root').sumBy('analysis.downstreamScore').value()

    node.analysis.totalScore = leafDescendentsScore + rootAncestorsScore + node.analysis.score
  }

  _.forEach(graph.nodes, (node) => { size(node.key) })
  _.filter(graph.nodes, 'analysis.root').forEach((root) => { downstreamScore(root.key) })
  _.filter(graph.nodes, 'analysis.leaf').forEach((leaf) => { upstreamScore(leaf.key) })
  _.forEach(graph.nodes, (node) => { totalScore(node.key) })
}

/**
 * Provides an understanding of jira issues and their network.
 *
 * @param {Object} jiraClient - The client to use to talk to JIRA
 * @param {Object?} dataStore - The data store to use
 * @param {Object} options - Options to customise behaviour
 * @param {string} options.query - The initial query to use
 * @param {string[]} options.followStatusCategories - An array of status categories to follow
 * @param {string[]} options.followLinkTypes - An array of link types to follow
 * @param {string[]} options.allowedRootIssueTypes - An array of types that are allowed to mark valid root issues
 * @param {string[]} options.allowedProjectKeys - An array of project keys that are allowed, or empty for any
 *
 * @property {Object[]} issues - all significant issues
 * @property {Object[]} graphs - array of distinct graphs containing 'nodes' and 'edges'
 * @property {Object[]} graphs.nodes - array of nodes in the graph, each element being an issue
 * @property {Object[]} graphs.edges - array of edges in the graph, each element being a link
 */
class Jirascope {
  constructor (jiraClient, dataStore, options) {
    this.jiraClient = jiraClient
    this.dataStore = dataStore
    this.options = options
    this.issues = []
    this.graphs = []
  }

  /**
   * Either reads or fetches issues, tidies, and analyses
   */
  async populate () {
    if (this.dataStore) {
      this.issues = await this.dataStore.readData('issues')
    }
    if (!this.issues) {
      await this.fetch()
    }
    this.tidy()
    this.analyse()
  }

  /**
   * Fetches issues from jira
   */
  async fetch () {
    const issuesByKey = {}
    logger.info(`fetching initial issues`)
    let newIssues = await this.jiraClient.getIssuesByQuery(this.options.query, 1000)
    newIssues.forEach((newIssue) => { issuesByKey[newIssue.key] = newIssue }) // initial issues we always keep

    while (newIssues.length > 0) {
      const { unvisitedIssueKeys, epicIssues } = processNewIssues.call(this, issuesByKey, newIssues)
      newIssues = await fetchNewIssues.call(this, unvisitedIssueKeys, epicIssues)
    }
    this.issues = Object.values(issuesByKey)
  }

  /**
   * Tidies up issues, such as pruning partial links and sorting by key numeric part
   */
  tidy () {
    const allIssuesByKey = _.keyBy(this.issues, 'key')
    // prune any partial links now we have fetched everything
    _.forEach(this.issues, (issue) => {
      issue.links = _.filter(issue.links, (link) => {
        return allIssuesByKey[link.srcKey] && allIssuesByKey[link.dstKey] // only include edges that we have issues for
      })
      issue.links = _.orderBy(issue.links, helpers.bySortableLinkKeys)
    })
    this.issues = _.orderBy(this.issues, helpers.bySortableKey)
  }

  /**
   * Analyses issues previously read or fetched and tidied
   */
  analyse () {
    const graphs = determineGraphs.call(this)
    graphs.forEach((graph) => { analyseGraph.call(this, graph) })
    this.graphs = _.orderBy(graphs, ['analysis.size'], ['desc'])
  }

  async store () {
    if (this.dataStore) {
      await this.dataStore.writeData('issues', this.issues)
      await this.dataStore.writeData('graphs', this.graphs)
    }
  }

  async cleanup () {
    this.issues = {}
    if (this.dataStore) {
      await this.dataStore.deleteData('issues')
      await this.dataStore.deleteData('graphs')
    }
  }

  get warningTypes () {
    return warningTypes
  }

  get helpers () {
    return helpers
  }
}

const warningTypes = {
  doneButBlocked: {
    label: 'Done But Blocked'
  },
  invalidRoot: {
    label: 'Invalid Root'
  },
  orphaned: {
    label: 'Orphaned'
  }
}

const helpers = {
  sortableKey: (key) => { return [_.split(key, '-')[0], _.padStart(_.split(key, '-')[1], 10, '0')].join('-') },
  bySortableKey: (x) => { return helpers.sortableKey(x.key) },
  bySortableLinkKeys: (x) => { return `${helpers.sortableKey(x.srcKey)}-${helpers.sortableKey(x.dstKey)}` },
  hasWarnings: (x) => { return x.analysis.warnings.length > 0 },
  isAllowedProjectKey: (key, allowedProjectKeys) => {
    if (allowedProjectKeys.length === 0) {
      return true
    }
    return _.some(allowedProjectKeys, (allowedProjectKey) => { return _.startsWith(key, allowedProjectKey) })
  }
}

Jirascope.helpers = helpers
Jirascope.warningTypes = warningTypes

exports = module.exports = Jirascope
<|endoftext|>


<commit_before>// Dependencies
var Barbe = require("barbe")
  , Fs = require("fs")
  , Path = require("path")
  , JxUtils = require("jxutils")
  , MarkDox = require("markdox")
  , IsThere = require("is-there")
  , ReadJson = require("r-json")
  , WriteJson = require("w-json")
  , ParentSearch = require("parent-search")
  , Streamp = require("streamp")
  , Ul = require("ul")
  , SameTime = require("same-time")
  ;

// Constants
const BLAH_DIR = ".blah"
    , README_NAME = "README.md"
    , TEMPLATES = "templates"
    , PACKAGE_JSON = "package.json"
    , INDEX_JSON = "index.json"
    , DOCUMENTATION_FILE_NAME = "DOCUMENTATION.md"
    ;

/*!
 * Blah
 * Creates a new `Blah` instance.
 *
 * @name Blah
 * @function
 */
function Blah(options) {
    if (typeof options === "string") {
        options = {
            path: options
        };
    }

    options = Ul.merge(options, {
        path: process.cwd()
      , templates_path: Path.join(Ul.USER_DIR, ".blah-templates")
    });

    this.blah_dir = Path.join(options.path, BLAH_DIR);
    this.blah_index = Path.join(this.blah_dir, INDEX_JSON);
    this.path = options.path;
    this.templates = {};
    this.pack = {};
    this.blah = {};

    if (IsThere(options.templates_path)) {
        this.templates_path = options.templates_path;
    } else {
        this.templates_path = Path.join(__dirname, TEMPLATES)
    }
}

/**
 * prepare
 * Prepares the Blah data.
 *
 * @name prepare
 * @function
 * @param {Function} callback The callback function.
 */
Blah.prototype.prepare = function (callback) {
    var self = this;
    SameTime([
        function (cb) {
            var tries = {
                local: Path.join(self.blah_dir, "markdox.ejs")
              , templ: Path.join(__dirname, "/markdox-res/template.ejs")
            };

            SameTime([
                IsThere.bind(this, tries.local)
              , IsThere.bind(this, tries.templ)
            ], function (exists) {
                cb(null, exists[0] ? tries.local : tries.templ);
            });
        }
      , function (cb) {
            ParentSearch(self.path, PACKAGE_JSON, { obj: true }, function (data) {
                cb(null, data);
            });
        }
        // Read the readme template
      , Fs.readFile.bind(self, Path.join(self.templates_path, README_NAME), "utf-8")
        // Read the .blah/index.json
      , function (cb) {
            ReadJson(self.blah_index, function (err, blahData) {
                if (err) {
                    err = null;
                    blahData = {};
                }
                cb(err, blahData);
            });
        }
    ], function (err, data) {
        if (err) { return callback(err); }
        self.markdox_tmpl = data[0];
        ReadJson(data[1].p, function (err, pack) {
            if (err) { return callback(err); }
            self.pack = pack;
            callback();
        });
        self.templates.readme = data[2];
        self.templates.blah = data[3];
    });
};

/**
 * init
 * Inits the `.blah` directory in the current project.
 *
 * @name init
 * @function
 * @param {Object} options An object containing the following fields:
 *
 *  - `readme` (Object): Data to render the `README.md` template.
 *  - `blah` (Object): An object which will be written to `.blah/index.json`
 *
 * @param {Function} callback The callback function.
 */
Blah.prototype.init = function (options, callback) {

    if (typeof options === "function") {
        callback = options;
        options = {};
    }

    options = Ul.merge(options, {
        blah: {}
      , readme: {}
    });

    var self = this
      , readme = new Streamp.writable(Path.join(self.blah_dir, README_NAME))
      ;

    // Run the init tasks
    SameTime([
        // Prepare data
        self.prepare.bind(self)
    ], function (err, data) {
        if (err) { return callback(err); }

        self.blah_data = Ul.merge(options.blah, self.templates.blah);

        SameTime([
            WriteJson.bind(self, self.blah_index, self.blah_data)
          , function (cb) {
                readme.end(Barbe(self.templates.readme, options.readme));
                callback();
            }
        ], callback);
    });
};

/**
 * generateDocs
 * Generate documentation file from package.json main file.
 *
 * @name generateDocs
 * @function
 * @param {String} input Input file name (default: main file from package.json)
 * @param {String} output Output file name (default: `DOCUMENTATION.md`)
 * @param {Function} callback The callback function
 */
Blah.prototype.docs = function (input, output, callback) {
    var self = this;
    if (typeof output === "function") {
        callback = output;
        output = DOCUMENTATION_FILE_NAME;
    }
    self.prepare(function (err) {
        if (err) { return callback(err); }
        MarkDox.process(input, {
            template: self.markdox_tmpl
          , output: output
        }, callback);
    });
};

/**
 * generateReadme
 * Returns a string representing the readme content of the project.
 *
 * @name generateReadme
 * @function
 * @param {Function} callback The callback function
 * @return {undefined}
 */
Blah.prototype.readme = function (callback) {

    var self = this;

    self.prepare(function (err) {
        if (err) { return callback(err); }
        SameTime([
            function (cb) {
                IsThere(self.pack.main, function (exists) {
                    if (!exists) { return cb(null, ""); }
                    MarkDox.process(self.pack.main, {
                        template: self.markdox_tmpl
                    }, function (err, docs) {
                        cb(err, docs);
                    });
                });
            }
        ], function (err, data) {
            if (err) { return callback(err); }
            content = Barbe(self.templates.readme, ["{", "}"], {
                pack: self.pack
              , docs: data[0]
            });
            callback(null, content);
        });
    });
};
//
///**
// * generateGitignore
// * Returns the content of .gitignore file
// *
// * @name generateGitignore
// * @function
// * @return {String} Content of gitignore file.
// */
//Blah.prototype.generateGitignore = function () {
//
//    var content =
//        "*.swp\n" +
//        "*.swo\n" +
//        "*~\n" +
//        "*.log\n" +
//        "node_modules"
//      ;
//
//    return content;
//};
//
///**
// * generateLicense
// * Returns the content of the LICENSE by providing the `@licenseName`.
// *
// * @name generateLicense
// * @function
// * @param {String} licenseName The license name (e.g. `mit`)
// * @return {String} The content of license.
// */
//Blah.prototype.generateLicense = function (licenseName) {
//
//    var fullName = null
//      , pack = require(process.env.PWD + "/package")
//      ;
//
//    try {
//        var gitconfigLines = Fs.readFileSync(
//            require('path-extra').homedir() + "/.gitconfig"
//        ).toString().replace(/\t/g, "").split("\n");
//        for (var i = 0; i < gitconfigLines.length; ++i) {
//            var cLine = gitconfigLines[i].trim();
//            if (/^name/.test(cLine)) {
//                fullName = cLine.split("=")[1].trim();
//                break;
//            }
//        }
//    } catch(e) {
//    }
//
//    if (!fullName) {
//        console.log("No fullname found in .gitconfig. Please modify LICENSE [fullname] manually");
//        fullName = "[fullname]";
//    }
//
//    return Fs
//        .readFileSync(__dirname + "/templates/licenses/" + licenseName.toLowerCase() + ".txt")
//        .toString()
//        .replace("[project]", pack.name)
//        .replace("[year]", new Date().getFullYear())
//        .replace("[fullname]", fullName)
//        .replace("[description]", pack.description)
//        ;
//};

module.exports = Blah;
<commit_msg>Added the search method<commit_after>// Dependencies
var Barbe = require("barbe")
  , Fs = require("fs")
  , Path = require("path")
  , JxUtils = require("jxutils")
  , MarkDox = require("markdox")
  , IsThere = require("is-there")
  , ReadJson = require("r-json")
  , WriteJson = require("w-json")
  , ParentSearch = require("parent-search")
  , Streamp = require("streamp")
  , Ul = require("ul")
  , SameTime = require("same-time")
  ;

// Constants
const BLAH_DIR = ".blah"
    , README_NAME = "README.md"
    , TEMPLATES = "templates"
    , PACKAGE_JSON = "package.json"
    , INDEX_JSON = "index.json"
    , DOCUMENTATION_FILE_NAME = "DOCUMENTATION.md"
    , MARKDOX_EJS = "markdox.ejs"
    ;

/*!
 * Blah
 * Creates a new `Blah` instance.
 *
 * @name Blah
 * @function
 */
function Blah(options) {
    if (typeof options === "string") {
        options = {
            path: options
        };
    }

    options = Ul.merge(options, {
        path: process.cwd()
      , templates_path: Path.join(Ul.USER_DIR, ".blah-templates")
    });

    this.blah_dir = Path.join(options.path, BLAH_DIR);
    this.blah_index = Path.join(this.blah_dir, INDEX_JSON);
    this.path = options.path;
    this.templates = {};
    this.pack = {};
    this.blah = {};

    if (IsThere(options.templates_path)) {
        this.templates_path = options.templates_path;
    } else {
        this.templates_path = Path.join(__dirname, TEMPLATES)
    }
}

Blah.prototype.search = function (file, callback) {
    var self = this
      , tries = [

            // Local .blah/file
            Path.join(self.blah_dir, file)

            // Templates: ~/.blah-templates/file
          , Path.join(self.templates_path, file)

            // Library templates: templates/file
          , Path.join(__dirname, "templates", file)
        ]
      ;

    SameTime(tries.map(function (cPath) {
        return function (cb) {
            IsThere(cPath, function (exists) {
                cb(null, cPath, exists);
            });
        }
    }), function (err, fields, exists) {
        if (err) { return callback(err); }
        for (var i = 0; i < exists.length; ++i) {
            if (exists[i]) {
                return callback(null, fields[i]);
            }
        }
    });
};

/**
 * prepare
 * Prepares the Blah data.
 *
 * @name prepare
 * @function
 * @param {Function} callback The callback function.
 */
Blah.prototype.prepare = function (callback) {
    var self = this;
    SameTime([
        self.search.bind(self, MARKDOX_EJS)
      , function (cb) {
            ParentSearch(self.path, PACKAGE_JSON, { obj: true }, function (data) {
                cb(null, data);
            });
        }
        // Read the readme template
      , Fs.readFile.bind(self, Path.join(self.templates_path, README_NAME), "utf-8")
        // Read the .blah/index.json
      , function (cb) {
            ReadJson(self.blah_index, function (err, blahData) {
                if (err) {
                    err = null;
                    blahData = {};
                }
                cb(err, blahData);
            });
        }
    ], function (err, data) {
        if (err) { return callback(err); }
        self.markdox_tmpl = data[0];
        ReadJson(data[1].p, function (err, pack) {
            if (err) { return callback(err); }
            self.pack = pack;
            callback();
        });
        self.templates.readme = data[2];
        self.templates.blah = data[3];
    });
};

/**
 * init
 * Inits the `.blah` directory in the current project.
 *
 * @name init
 * @function
 * @param {Object} options An object containing the following fields:
 *
 *  - `readme` (Object): Data to render the `README.md` template.
 *  - `blah` (Object): An object which will be written to `.blah/index.json`
 *
 * @param {Function} callback The callback function.
 */
Blah.prototype.init = function (options, callback) {

    if (typeof options === "function") {
        callback = options;
        options = {};
    }

    options = Ul.merge(options, {
        blah: {}
      , readme: {}
    });

    var self = this
      , readme = new Streamp.writable(Path.join(self.blah_dir, README_NAME))
      ;

    // Run the init tasks
    SameTime([
        // Prepare data
        self.prepare.bind(self)
    ], function (err, data) {
        if (err) { return callback(err); }

        self.blah_data = Ul.merge(options.blah, self.templates.blah);

        SameTime([
            WriteJson.bind(self, self.blah_index, self.blah_data)
          , function (cb) {
                readme.end(Barbe(self.templates.readme, options.readme));
                callback();
            }
        ], callback);
    });
};

/**
 * generateDocs
 * Generate documentation file from package.json main file.
 *
 * @name generateDocs
 * @function
 * @param {String} input Input file name (default: main file from package.json)
 * @param {String} output Output file name (default: `DOCUMENTATION.md`)
 * @param {Function} callback The callback function
 */
Blah.prototype.docs = function (input, output, callback) {
    var self = this;
    if (typeof output === "function") {
        callback = output;
        output = DOCUMENTATION_FILE_NAME;
    }
    self.prepare(function (err) {
        if (err) { return callback(err); }
        MarkDox.process(input, {
            template: self.markdox_tmpl
          , output: output
        }, callback);
    });
};

/**
 * generateReadme
 * Returns a string representing the readme content of the project.
 *
 * @name generateReadme
 * @function
 * @param {Function} callback The callback function
 * @return {undefined}
 */
Blah.prototype.readme = function (callback) {

    var self = this;

    self.prepare(function (err) {
        if (err) { return callback(err); }
        SameTime([
            function (cb) {
                IsThere(self.pack.main, function (exists) {
                    if (!exists) { return cb(null, ""); }
                    MarkDox.process(self.pack.main, {
                        template: self.markdox_tmpl
                    }, function (err, docs) {
                        cb(err, docs);
                    });
                });
            }
        ], function (err, data) {
            if (err) { return callback(err); }
            content = Barbe(self.templates.readme, ["{", "}"], {
                pack: self.pack
              , docs: data[0]
            });
            Fs.writeFile("README.md", content, callback);
        });
    });
};

/**
 * generateGitignore
 * Returns the content of .gitignore file
 *
 * @name generateGitignore
 * @function
 * @return {String} Content of gitignore file.
 */
Blah.prototype.generateGitignore = function () {

    var content =
        "*.swp\n" +
        "*.swo\n" +
        "*~\n" +
        "*.log\n" +
        "node_modules"
      ;

    return content;
};
//
///**
// * generateLicense
// * Returns the content of the LICENSE by providing the `@licenseName`.
// *
// * @name generateLicense
// * @function
// * @param {String} licenseName The license name (e.g. `mit`)
// * @return {String} The content of license.
// */
//Blah.prototype.generateLicense = function (licenseName) {
//
//    var fullName = null
//      , pack = require(process.env.PWD + "/package")
//      ;
//
//    try {
//        var gitconfigLines = Fs.readFileSync(
//            require('path-extra').homedir() + "/.gitconfig"
//        ).toString().replace(/\t/g, "").split("\n");
//        for (var i = 0; i < gitconfigLines.length; ++i) {
//            var cLine = gitconfigLines[i].trim();
//            if (/^name/.test(cLine)) {
//                fullName = cLine.split("=")[1].trim();
//                break;
//            }
//        }
//    } catch(e) {
//    }
//
//    if (!fullName) {
//        console.log("No fullname found in .gitconfig. Please modify LICENSE [fullname] manually");
//        fullName = "[fullname]";
//    }
//
//    return Fs
//        .readFileSync(__dirname + "/templates/licenses/" + licenseName.toLowerCase() + ".txt")
//        .toString()
//        .replace("[project]", pack.name)
//        .replace("[year]", new Date().getFullYear())
//        .replace("[fullname]", fullName)
//        .replace("[description]", pack.description)
//        ;
//};

module.exports = Blah;
<|endoftext|>


<commit_before>
/**
 * Module dependencies.
 */

var http = require('http');
var read = require('fs').readFileSync;
var engine = require('engine.io');
var client = require('socket.io-client');
var clientVersion = require('socket.io-client/package').version;
var Client = require('./client');
var Emitter = require('events').EventEmitter;
var Namespace = require('./namespace');
var Adapter = require('socket.io-adapter');
var debug = require('debug')('socket.io:server');
var url = require('url');

/**
 * Module exports.
 */

module.exports = Server;

/**
 * Socket.IO client source.
 */

var clientSource = undefined;
var clientSourceMap = undefined;

/**
 * Server constructor.
 *
 * @param {http.Server|Number|Object} srv http server, port or options
 * @param {Object} [opts]
 * @api public
 */

function Server(srv, opts){
  if (!(this instanceof Server)) return new Server(srv, opts);
  if ('object' == typeof srv && !srv.listen) {
    opts = srv;
    srv = null;
  }
  opts = opts || {};
  this.nsps = {};
  this.path(opts.path || '/socket.io');
  this.serveClient(false !== opts.serveClient);
  this.adapter(opts.adapter || Adapter);
  this.origins(opts.origins || '*:*');
  this.sockets = this.of('/');
  if (srv) this.attach(srv, opts);
}

/**
 * Server request verification function, that checks for allowed origins
 *
 * @param {http.IncomingMessage} req request
 * @param {Function} fn callback to be called with the result: `fn(err, success)`
 */

Server.prototype.checkRequest = function(req, fn) {
  var origin = req.headers.origin || req.headers.referer;

  // file:// URLs produce a null Origin which can't be authorized via echo-back
  if ('null' == origin || null == origin) origin = '*';

  if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn);
  if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
  if (origin) {
    try {
      var parts = url.parse(origin);
      var defaultPort = 'https:' == parts.protocol ? 443 : 80;
      parts.port = parts.port != null
        ? parts.port
        : defaultPort;
      var ok =
        ~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
        ~this._origins.indexOf(parts.hostname + ':*') ||
        ~this._origins.indexOf('*:' + parts.port);
      return fn(null, !!ok);
    } catch (ex) {
    }
  }
  fn(null, false);
};

/**
 * Sets/gets whether client code is being served.
 *
 * @param {Boolean} v whether to serve client code
 * @return {Server|Boolean} self when setting or value when getting
 * @api public
 */

Server.prototype.serveClient = function(v){
  if (!arguments.length) return this._serveClient;
  this._serveClient = v;

  if (v && !clientSource) {
    clientSource = read(require.resolve('socket.io-client/dist/socket.io.min.js'), 'utf-8');
    try {
      clientSourceMap = read(require.resolve('socket.io-client/dist/socket.io.js.map'), 'utf-8');
    } catch(err) {
      debug('could not load sourcemap file');
    }
  }

  return this;
};

/**
 * Old settings for backwards compatibility
 */

var oldSettings = {
  "transports": "transports",
  "heartbeat timeout": "pingTimeout",
  "heartbeat interval": "pingInterval",
  "destroy buffer size": "maxHttpBufferSize"
};

/**
 * Backwards compatibility.
 *
 * @api public
 */

Server.prototype.set = function(key, val){
  if ('authorization' == key && val) {
    this.use(function(socket, next) {
      val(socket.request, function(err, authorized) {
        if (err) return next(new Error(err));
        if (!authorized) return next(new Error('Not authorized'));
        next();
      });
    });
  } else if ('origins' == key && val) {
    this.origins(val);
  } else if ('resource' == key) {
    this.path(val);
  } else if (oldSettings[key] && this.eio[oldSettings[key]]) {
    this.eio[oldSettings[key]] = val;
  } else {
    console.error('Option %s is not valid. Please refer to the README.', key);
  }

  return this;
};

/**
 * Sets the client serving path.
 *
 * @param {String} v pathname
 * @return {Server|String} self when setting or value when getting
 * @api public
 */

Server.prototype.path = function(v){
  if (!arguments.length) return this._path;
  this._path = v.replace(/\/$/, '');
  return this;
};

/**
 * Sets the adapter for rooms.
 *
 * @param {Adapter} v pathname
 * @return {Server|Adapter} self when setting or value when getting
 * @api public
 */

Server.prototype.adapter = function(v){
  if (!arguments.length) return this._adapter;
  this._adapter = v;
  for (var i in this.nsps) {
    if (this.nsps.hasOwnProperty(i)) {
      this.nsps[i].initAdapter();
    }
  }
  return this;
};

/**
 * Sets the allowed origins for requests.
 *
 * @param {String} v origins
 * @return {Server|Adapter} self when setting or value when getting
 * @api public
 */

Server.prototype.origins = function(v){
  if (!arguments.length) return this._origins;

  this._origins = v;
  return this;
};

/**
 * Attaches socket.io to a server or port.
 *
 * @param {http.Server|Number} server or port
 * @param {Object} options passed to engine.io
 * @return {Server} self
 * @api public
 */

Server.prototype.listen =
Server.prototype.attach = function(srv, opts){
  if ('function' == typeof srv) {
    var msg = 'You are trying to attach socket.io to an express ' +
    'request handler function. Please pass a http.Server instance.';
    throw new Error(msg);
  }

  // handle a port as a string
  if (Number(srv) == srv) {
    srv = Number(srv);
  }

  if ('number' == typeof srv) {
    debug('creating http server and binding to %d', srv);
    var port = srv;
    srv = http.Server(function(req, res){
      res.writeHead(404);
      res.end();
    });
    srv.listen(port);

  }

  // set engine.io path to `/socket.io`
  opts = opts || {};
  opts.path = opts.path || this.path();
  // set origins verification
  opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this);

  // initialize engine
  debug('creating engine.io instance with opts %j', opts);
  this.eio = engine.attach(srv, opts);

  // attach static file serving
  if (this._serveClient) this.attachServe(srv);

  // Export http server
  this.httpServer = srv;

  // bind to engine events
  this.bind(this.eio);

  return this;
};

/**
 * Attaches the static file serving.
 *
 * @param {Function|http.Server} srv http server
 * @api private
 */

Server.prototype.attachServe = function(srv){
  debug('attaching client serving req handler');
  var url = this._path + '/socket.io.js';
  var urlMap = this._path + '/socket.io.js.map';
  var evs = srv.listeners('request').slice(0);
  var self = this;
  srv.removeAllListeners('request');
  srv.on('request', function(req, res) {
    if (0 === req.url.indexOf(urlMap)) {
      self.serveMap(req, res);
    } else if (0 === req.url.indexOf(url)) {
      self.serve(req, res);
    } else {
      for (var i = 0; i < evs.length; i++) {
        evs[i].call(srv, req, res);
      }
    }
  });
};

/**
 * Handles a request serving `/socket.io.js`
 *
 * @param {http.Request} req
 * @param {http.Response} res
 * @api private
 */

Server.prototype.serve = function(req, res){
  // Per the standard, ETags must be quoted:
  // https://tools.ietf.org/html/rfc7232#section-2.3
  var expectedEtag = '"' + clientVersion + '"';

  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }

  debug('serve client source');
  res.setHeader('Content-Type', 'application/javascript');
  res.setHeader('ETag', expectedEtag);
  res.setHeader('X-SourceMap', 'socket.io.js.map');
  res.writeHead(200);
  res.end(clientSource);
};

/**
 * Handles a request serving `/socket.io.js.map`
 *
 * @param {http.Request} req
 * @param {http.Response} res
 * @api private
 */

Server.prototype.serveMap = function(req, res){
  // Per the standard, ETags must be quoted:
  // https://tools.ietf.org/html/rfc7232#section-2.3
  var expectedEtag = '"' + clientVersion + '"';

  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }

  debug('serve client sourcemap');
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('ETag', expectedEtag);
  res.writeHead(200);
  res.end(clientSourceMap);
};

/**
 * Binds socket.io to an engine.io instance.
 *
 * @param {engine.Server} engine engine.io (or compatible) server
 * @return {Server} self
 * @api public
 */

Server.prototype.bind = function(engine){
  this.engine = engine;
  this.engine.on('connection', this.onconnection.bind(this));
  return this;
};

/**
 * Called with each incoming transport connection.
 *
 * @param {engine.Socket} conn
 * @return {Server} self
 * @api public
 */

Server.prototype.onconnection = function(conn){
  debug('incoming connection with id %s', conn.id);
  var client = new Client(this, conn);
  client.connect('/');
  return this;
};

/**
 * Looks up a namespace.
 *
 * @param {String} name nsp name
 * @param {Function} [fn] optional, nsp `connection` ev handler
 * @api public
 */

Server.prototype.of = function(name, fn){
  if (String(name)[0] !== '/') name = '/' + name;

  var nsp = this.nsps[name];
  if (!nsp) {
    debug('initializing namespace %s', name);
    nsp = new Namespace(this, name);
    this.nsps[name] = nsp;
  }
  if (fn) nsp.on('connect', fn);
  return nsp;
};

/**
 * Closes server connection
 *
 * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed
 * @api public
 */

Server.prototype.close = function(fn){
  for (var id in this.nsps['/'].sockets) {
    if (this.nsps['/'].sockets.hasOwnProperty(id)) {
      this.nsps['/'].sockets[id].onclose();
    }
  }

  this.engine.close();

  if (this.httpServer) {
    this.httpServer.close(fn);
  } else {
    fn && fn();
  }
};

/**
 * Expose main namespace (/).
 */

var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
  return typeof Emitter.prototype[key] === 'function';
});

emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress']).forEach(function(fn){
  Server.prototype[fn] = function(){
    return this.sockets[fn].apply(this.sockets, arguments);
  };
});

Namespace.flags.forEach(function(flag){
  Object.defineProperty(Server.prototype, flag, {
    get: function() {
      this.sockets.flags = this.sockets.flags || {};
      this.sockets.flags[flag] = true;
      return this;
    }
  });
});

/**
 * BC with `io.listen`
 */

Server.listen = Server;
<commit_msg>[fix] Prevent null from being accepted as argument (#2606)<commit_after>
/**
 * Module dependencies.
 */

var http = require('http');
var read = require('fs').readFileSync;
var engine = require('engine.io');
var client = require('socket.io-client');
var clientVersion = require('socket.io-client/package').version;
var Client = require('./client');
var Emitter = require('events').EventEmitter;
var Namespace = require('./namespace');
var Adapter = require('socket.io-adapter');
var debug = require('debug')('socket.io:server');
var url = require('url');

/**
 * Module exports.
 */

module.exports = Server;

/**
 * Socket.IO client source.
 */

var clientSource = undefined;
var clientSourceMap = undefined;

/**
 * Server constructor.
 *
 * @param {http.Server|Number|Object} srv http server, port or options
 * @param {Object} [opts]
 * @api public
 */

function Server(srv, opts){
  if (!(this instanceof Server)) return new Server(srv, opts);
  if ('object' == typeof srv && srv instanceof Object && !srv.listen) {
    opts = srv;
    srv = null;
  }
  opts = opts || {};
  this.nsps = {};
  this.path(opts.path || '/socket.io');
  this.serveClient(false !== opts.serveClient);
  this.adapter(opts.adapter || Adapter);
  this.origins(opts.origins || '*:*');
  this.sockets = this.of('/');
  if (srv) this.attach(srv, opts);
}

/**
 * Server request verification function, that checks for allowed origins
 *
 * @param {http.IncomingMessage} req request
 * @param {Function} fn callback to be called with the result: `fn(err, success)`
 */

Server.prototype.checkRequest = function(req, fn) {
  var origin = req.headers.origin || req.headers.referer;

  // file:// URLs produce a null Origin which can't be authorized via echo-back
  if ('null' == origin || null == origin) origin = '*';

  if (!!origin && typeof(this._origins) == 'function') return this._origins(origin, fn);
  if (this._origins.indexOf('*:*') !== -1) return fn(null, true);
  if (origin) {
    try {
      var parts = url.parse(origin);
      var defaultPort = 'https:' == parts.protocol ? 443 : 80;
      parts.port = parts.port != null
        ? parts.port
        : defaultPort;
      var ok =
        ~this._origins.indexOf(parts.hostname + ':' + parts.port) ||
        ~this._origins.indexOf(parts.hostname + ':*') ||
        ~this._origins.indexOf('*:' + parts.port);
      return fn(null, !!ok);
    } catch (ex) {
    }
  }
  fn(null, false);
};

/**
 * Sets/gets whether client code is being served.
 *
 * @param {Boolean} v whether to serve client code
 * @return {Server|Boolean} self when setting or value when getting
 * @api public
 */

Server.prototype.serveClient = function(v){
  if (!arguments.length) return this._serveClient;
  this._serveClient = v;

  if (v && !clientSource) {
    clientSource = read(require.resolve('socket.io-client/dist/socket.io.min.js'), 'utf-8');
    try {
      clientSourceMap = read(require.resolve('socket.io-client/dist/socket.io.js.map'), 'utf-8');
    } catch(err) {
      debug('could not load sourcemap file');
    }
  }

  return this;
};

/**
 * Old settings for backwards compatibility
 */

var oldSettings = {
  "transports": "transports",
  "heartbeat timeout": "pingTimeout",
  "heartbeat interval": "pingInterval",
  "destroy buffer size": "maxHttpBufferSize"
};

/**
 * Backwards compatibility.
 *
 * @api public
 */

Server.prototype.set = function(key, val){
  if ('authorization' == key && val) {
    this.use(function(socket, next) {
      val(socket.request, function(err, authorized) {
        if (err) return next(new Error(err));
        if (!authorized) return next(new Error('Not authorized'));
        next();
      });
    });
  } else if ('origins' == key && val) {
    this.origins(val);
  } else if ('resource' == key) {
    this.path(val);
  } else if (oldSettings[key] && this.eio[oldSettings[key]]) {
    this.eio[oldSettings[key]] = val;
  } else {
    console.error('Option %s is not valid. Please refer to the README.', key);
  }

  return this;
};

/**
 * Sets the client serving path.
 *
 * @param {String} v pathname
 * @return {Server|String} self when setting or value when getting
 * @api public
 */

Server.prototype.path = function(v){
  if (!arguments.length) return this._path;
  this._path = v.replace(/\/$/, '');
  return this;
};

/**
 * Sets the adapter for rooms.
 *
 * @param {Adapter} v pathname
 * @return {Server|Adapter} self when setting or value when getting
 * @api public
 */

Server.prototype.adapter = function(v){
  if (!arguments.length) return this._adapter;
  this._adapter = v;
  for (var i in this.nsps) {
    if (this.nsps.hasOwnProperty(i)) {
      this.nsps[i].initAdapter();
    }
  }
  return this;
};

/**
 * Sets the allowed origins for requests.
 *
 * @param {String} v origins
 * @return {Server|Adapter} self when setting or value when getting
 * @api public
 */

Server.prototype.origins = function(v){
  if (!arguments.length) return this._origins;

  this._origins = v;
  return this;
};

/**
 * Attaches socket.io to a server or port.
 *
 * @param {http.Server|Number} server or port
 * @param {Object} options passed to engine.io
 * @return {Server} self
 * @api public
 */

Server.prototype.listen =
Server.prototype.attach = function(srv, opts){
  if ('function' == typeof srv) {
    var msg = 'You are trying to attach socket.io to an express ' +
    'request handler function. Please pass a http.Server instance.';
    throw new Error(msg);
  }

  // handle a port as a string
  if (Number(srv) == srv) {
    srv = Number(srv);
  }

  if ('number' == typeof srv) {
    debug('creating http server and binding to %d', srv);
    var port = srv;
    srv = http.Server(function(req, res){
      res.writeHead(404);
      res.end();
    });
    srv.listen(port);

  }

  // set engine.io path to `/socket.io`
  opts = opts || {};
  opts.path = opts.path || this.path();
  // set origins verification
  opts.allowRequest = opts.allowRequest || this.checkRequest.bind(this);

  // initialize engine
  debug('creating engine.io instance with opts %j', opts);
  this.eio = engine.attach(srv, opts);

  // attach static file serving
  if (this._serveClient) this.attachServe(srv);

  // Export http server
  this.httpServer = srv;

  // bind to engine events
  this.bind(this.eio);

  return this;
};

/**
 * Attaches the static file serving.
 *
 * @param {Function|http.Server} srv http server
 * @api private
 */

Server.prototype.attachServe = function(srv){
  debug('attaching client serving req handler');
  var url = this._path + '/socket.io.js';
  var urlMap = this._path + '/socket.io.js.map';
  var evs = srv.listeners('request').slice(0);
  var self = this;
  srv.removeAllListeners('request');
  srv.on('request', function(req, res) {
    if (0 === req.url.indexOf(urlMap)) {
      self.serveMap(req, res);
    } else if (0 === req.url.indexOf(url)) {
      self.serve(req, res);
    } else {
      for (var i = 0; i < evs.length; i++) {
        evs[i].call(srv, req, res);
      }
    }
  });
};

/**
 * Handles a request serving `/socket.io.js`
 *
 * @param {http.Request} req
 * @param {http.Response} res
 * @api private
 */

Server.prototype.serve = function(req, res){
  // Per the standard, ETags must be quoted:
  // https://tools.ietf.org/html/rfc7232#section-2.3
  var expectedEtag = '"' + clientVersion + '"';

  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }

  debug('serve client source');
  res.setHeader('Content-Type', 'application/javascript');
  res.setHeader('ETag', expectedEtag);
  res.setHeader('X-SourceMap', 'socket.io.js.map');
  res.writeHead(200);
  res.end(clientSource);
};

/**
 * Handles a request serving `/socket.io.js.map`
 *
 * @param {http.Request} req
 * @param {http.Response} res
 * @api private
 */

Server.prototype.serveMap = function(req, res){
  // Per the standard, ETags must be quoted:
  // https://tools.ietf.org/html/rfc7232#section-2.3
  var expectedEtag = '"' + clientVersion + '"';

  var etag = req.headers['if-none-match'];
  if (etag) {
    if (expectedEtag == etag) {
      debug('serve client 304');
      res.writeHead(304);
      res.end();
      return;
    }
  }

  debug('serve client sourcemap');
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('ETag', expectedEtag);
  res.writeHead(200);
  res.end(clientSourceMap);
};

/**
 * Binds socket.io to an engine.io instance.
 *
 * @param {engine.Server} engine engine.io (or compatible) server
 * @return {Server} self
 * @api public
 */

Server.prototype.bind = function(engine){
  this.engine = engine;
  this.engine.on('connection', this.onconnection.bind(this));
  return this;
};

/**
 * Called with each incoming transport connection.
 *
 * @param {engine.Socket} conn
 * @return {Server} self
 * @api public
 */

Server.prototype.onconnection = function(conn){
  debug('incoming connection with id %s', conn.id);
  var client = new Client(this, conn);
  client.connect('/');
  return this;
};

/**
 * Looks up a namespace.
 *
 * @param {String} name nsp name
 * @param {Function} [fn] optional, nsp `connection` ev handler
 * @api public
 */

Server.prototype.of = function(name, fn){
  if (String(name)[0] !== '/') name = '/' + name;

  var nsp = this.nsps[name];
  if (!nsp) {
    debug('initializing namespace %s', name);
    nsp = new Namespace(this, name);
    this.nsps[name] = nsp;
  }
  if (fn) nsp.on('connect', fn);
  return nsp;
};

/**
 * Closes server connection
 *
 * @param {Function} [fn] optional, called as `fn([err])` on error OR all conns closed
 * @api public
 */

Server.prototype.close = function(fn){
  for (var id in this.nsps['/'].sockets) {
    if (this.nsps['/'].sockets.hasOwnProperty(id)) {
      this.nsps['/'].sockets[id].onclose();
    }
  }

  this.engine.close();

  if (this.httpServer) {
    this.httpServer.close(fn);
  } else {
    fn && fn();
  }
};

/**
 * Expose main namespace (/).
 */

var emitterMethods = Object.keys(Emitter.prototype).filter(function(key){
  return typeof Emitter.prototype[key] === 'function';
});

emitterMethods.concat(['to', 'in', 'use', 'send', 'write', 'clients', 'compress']).forEach(function(fn){
  Server.prototype[fn] = function(){
    return this.sockets[fn].apply(this.sockets, arguments);
  };
});

Namespace.flags.forEach(function(flag){
  Object.defineProperty(Server.prototype, flag, {
    get: function() {
      this.sockets.flags = this.sockets.flags || {};
      this.sockets.flags[flag] = true;
      return this;
    }
  });
});

/**
 * BC with `io.listen`
 */

Server.listen = Server;
<|endoftext|>


<commit_before>/*
 * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

var express = require('express'),
  _ = require('lodash'),
  debug = require('debug')('signalk-server'),
  path = require('path'),
  http = require('http'),
  https = require('https'),
  httpolyglot = require('httpolyglot'),
  pem = require('pem'),
  fs = require('fs'),
  FullSignalK = require('signalk-schema').FullSignalK,
  StreamBundle = require('./streambundle'),
  SubscriptionManager = require('./subscriptionmanager'),
  Mode = require('stat-mode')

function Server(opts) {
  this.params = opts || {};
  this.app = express();
  this.app.started = false;

  this.app.overrides = {
    settings: this.params.settings || null,
    port: this.params.port || null
  };

  var app = this.app;
  require('./config/config')(app);
  app.selfContext = 'vessels.' + app.selfId;
  app.version = "0.0.1"

  app.use(require('body-parser').json())
  app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html')
  })

  app.signalk = new FullSignalK(app.selfId, app.selfType, app.config.defaults);

  app.streambundle = new StreamBundle(app.selfId);
  app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle))
  app.subscriptionmanager = new SubscriptionManager(app);
}

module.exports = Server;

Server.prototype.start = function() {
  var self = this;
  var app = this.app;

  this.app.pruneInterval = setInterval(app.signalk.pruneContexts.bind(app.signalk, 60 * 60), 60 * 1000);
  this.app.providers = [];

  return new Promise((resolve, reject) => {
    createServer(app, function(err, server) {
      if (err) {
        reject(err)
        return
      }
      app.server = server;
      app.interfaces = {};
      app.connections = {};
      app.clients = 0;

      debug("ID type: " + app.selfType);
      debug("ID: " + app.selfId);

      startInterfaces(app);
      startMdns(app);
      app.providers = require('./pipedproviders')(app).start();

      var SD_LISTEN_FDS_START = 3
      var port = process.env.LISTEN_FDS > 0 ?
        {
          fd: SD_LISTEN_FDS_START
        } :
        app.config.port;

      server.listen(port, function() {
        console.log('signalk-server running at 0.0.0.0:' + port + "\n");
        app.started = true;
        resolve(self);
      });
    })
  })
};

function createServer(app, cb) {
  if(typeof app.config.settings.ssl === "undefined" || app.config.settings.ssl) {
    getCertificateOptions(function(err, options) {
      if(err) {
        cb(err)
      } else {
        debug("Starting server to serve both http and https")
        cb(null, httpolyglot.createServer(options, app));
      }
    });
    return;
  };
  var server;
  try {
    debug("Starting server to serve only http")
    server = http.createServer(app);
  } catch(e) {
    cb(e);
    return;
  }
  cb(null, server);
}

function hasStrictPermissions(stat) {
  return new Mode(stat).toString() === '-r--------'
}

function getCertificateOptions(cb) {
  if(fs.existsSync('./settings/ssl-key.pem') && fs.existsSync('./settings/ssl-cert.pem')) {
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-key.pem'))) {
      cb(new Error('./settings/ssl-key.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-cert.pem'))) {
      cb(new Error('./settings/ssl-cert.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    debug("Using certificate ssl-key.pem and ssl-cert.pem in ./settings/");
    cb(null, {
      key: fs.readFileSync('./settings/ssl-key.pem'),
      cert: fs.readFileSync('./settings/ssl-cert.pem')
    });
    return;
  } else {
    createCertificateOptions(cb);
  }
}

function createCertificateOptions(cb) {
  debug("Creating certificate files in ./settings/");
  pem.createCertificate({
    days: 360,
    selfSigned: true
  }, function(err, keys) {
    if(err) {
      console.error("Could not create SSL certificate:" + err.message)
      throw err
    } else {
      fs.writeFileSync('./settings/ssl-key.pem', keys.serviceKey);
      fs.chmodSync('./settings/ssl-key.pem', '400');
      fs.writeFileSync('./settings/ssl-cert.pem', keys.certificate);
      fs.chmodSync('./settings/ssl-cert.pem', '400');
      cb(null, {
        key: keys.serviceKey,
        cert: keys.certificate
      })
    }
  });
}

function startMdns(app) {
  if(_.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) {
    debug("Starting interface 'mDNS'");
    try {
      app.interfaces['mdns'] = require('./mdns')(app);
    } catch(ex) {
      debug("Could not start mDNS:" + ex);

    }
  } else {
    debug("Interface 'mDNS' was disabled in configuration");
  }
}

function startInterfaces(app) {
  debug("Interfaces config:" + JSON.stringify(app.config.settings.interfaces));
  var availableInterfaces = require('./interfaces');
  _.forIn(availableInterfaces, function(interface, name) {
    if(_.isUndefined(app.config.settings.interfaces) ||
      _.isUndefined(app.config.settings.interfaces[name]) ||
      app.config.settings.interfaces[name]) {
      debug("Loading interface '" + name + "'");
      app.interfaces[name] = interface(app);
      if(app.interfaces[name] && _.isFunction(app.interfaces[name].start)) {
        debug("Starting interface '" + name + "'");
        app.interfaces[name].start();
      }
    } else {
      debug("Not loading interface '" + name + "' because of configuration");
    }
  });
}

Server.prototype.reload = function(mixed) {
  var settings, self = this;

  if(typeof mixed === 'string') {
    try {
      settings = require(path.join(process.cwd(), mixed));
    } catch(e) {
      debug("Settings file '" + settings + "' doesn't exist.");
    }
  }

  if(mixed !== null && typeof mixed === 'object') {
    settings = mixed;
  }

  if(settings) {
    this.app.config.settings = settings;
  }

  this.stop();

  setTimeout(function() {
    self.start();
  }, 1000);

  return this;
};

Server.prototype.stop = function(cb) {
  if(this.app.started === true) {
    _.each(this.app.interfaces, function(intf) {
      if(intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') {
        intf.stop();
      }
    });

    debug("Closing server...");

    this.app.server.close(function() {
      debug("Server closed...");
      this.server = null;
      cb();
    });

    for(var id in this.app.connections) {
      if(this.app.connections.hasOwnProperty(id)) {
        debug("Closing connection #" + id);
        this.app.connections[id].destroy();
        delete this.app.connections[id];
      }
    }

    if(this.app.pruneInterval) {
      clearInterval(this.app.pruneInterval);
    }
    this.app.started = false;
  }

  this.app.providers.forEach(function(providerHolder) {
    providerHolder.pipeElements[0].end();
  });

  return this;
};
<commit_msg>feature: add check for minimum Node version<commit_after>/*
 * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>
 *
 * 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.
*/

if (typeof [].includes != 'function') {
  console.log("Minimum required Node.js version is 6, please update.")
  process.exit(-1)
}

var express = require('express'),
  _ = require('lodash'),
  debug = require('debug')('signalk-server'),
  path = require('path'),
  http = require('http'),
  https = require('https'),
  httpolyglot = require('httpolyglot'),
  pem = require('pem'),
  fs = require('fs'),
  FullSignalK = require('signalk-schema').FullSignalK,
  StreamBundle = require('./streambundle'),
  SubscriptionManager = require('./subscriptionmanager'),
  Mode = require('stat-mode')

function Server(opts) {
  this.params = opts || {};
  this.app = express();
  this.app.started = false;

  this.app.overrides = {
    settings: this.params.settings || null,
    port: this.params.port || null
  };

  var app = this.app;
  require('./config/config')(app);
  app.selfContext = 'vessels.' + app.selfId;
  app.version = "0.0.1"

  app.use(require('body-parser').json())
  app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html')
  })

  app.signalk = new FullSignalK(app.selfId, app.selfType, app.config.defaults);

  app.streambundle = new StreamBundle(app.selfId);
  app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle))
  app.subscriptionmanager = new SubscriptionManager(app);
}

module.exports = Server;

Server.prototype.start = function() {
  var self = this;
  var app = this.app;

  this.app.pruneInterval = setInterval(app.signalk.pruneContexts.bind(app.signalk, 60 * 60), 60 * 1000);
  this.app.providers = [];

  return new Promise((resolve, reject) => {
    createServer(app, function(err, server) {
      if (err) {
        reject(err)
        return
      }
      app.server = server;
      app.interfaces = {};
      app.connections = {};
      app.clients = 0;

      debug("ID type: " + app.selfType);
      debug("ID: " + app.selfId);

      startInterfaces(app);
      startMdns(app);
      app.providers = require('./pipedproviders')(app).start();

      var SD_LISTEN_FDS_START = 3
      var port = process.env.LISTEN_FDS > 0 ?
        {
          fd: SD_LISTEN_FDS_START
        } :
        app.config.port;

      server.listen(port, function() {
        console.log('signalk-server running at 0.0.0.0:' + port + "\n");
        app.started = true;
        resolve(self);
      });
    })
  })
};

function createServer(app, cb) {
  if(typeof app.config.settings.ssl === "undefined" || app.config.settings.ssl) {
    getCertificateOptions(function(err, options) {
      if(err) {
        cb(err)
      } else {
        debug("Starting server to serve both http and https")
        cb(null, httpolyglot.createServer(options, app));
      }
    });
    return;
  };
  var server;
  try {
    debug("Starting server to serve only http")
    server = http.createServer(app);
  } catch(e) {
    cb(e);
    return;
  }
  cb(null, server);
}

function hasStrictPermissions(stat) {
  return new Mode(stat).toString() === '-r--------'
}

function getCertificateOptions(cb) {
  if(fs.existsSync('./settings/ssl-key.pem') && fs.existsSync('./settings/ssl-cert.pem')) {
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-key.pem'))) {
      cb(new Error('./settings/ssl-key.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-cert.pem'))) {
      cb(new Error('./settings/ssl-cert.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    debug("Using certificate ssl-key.pem and ssl-cert.pem in ./settings/");
    cb(null, {
      key: fs.readFileSync('./settings/ssl-key.pem'),
      cert: fs.readFileSync('./settings/ssl-cert.pem')
    });
    return;
  } else {
    createCertificateOptions(cb);
  }
}

function createCertificateOptions(cb) {
  debug("Creating certificate files in ./settings/");
  pem.createCertificate({
    days: 360,
    selfSigned: true
  }, function(err, keys) {
    if(err) {
      console.error("Could not create SSL certificate:" + err.message)
      throw err
    } else {
      fs.writeFileSync('./settings/ssl-key.pem', keys.serviceKey);
      fs.chmodSync('./settings/ssl-key.pem', '400');
      fs.writeFileSync('./settings/ssl-cert.pem', keys.certificate);
      fs.chmodSync('./settings/ssl-cert.pem', '400');
      cb(null, {
        key: keys.serviceKey,
        cert: keys.certificate
      })
    }
  });
}

function startMdns(app) {
  if(_.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) {
    debug("Starting interface 'mDNS'");
    try {
      app.interfaces['mdns'] = require('./mdns')(app);
    } catch(ex) {
      debug("Could not start mDNS:" + ex);

    }
  } else {
    debug("Interface 'mDNS' was disabled in configuration");
  }
}

function startInterfaces(app) {
  debug("Interfaces config:" + JSON.stringify(app.config.settings.interfaces));
  var availableInterfaces = require('./interfaces');
  _.forIn(availableInterfaces, function(interface, name) {
    if(_.isUndefined(app.config.settings.interfaces) ||
      _.isUndefined(app.config.settings.interfaces[name]) ||
      app.config.settings.interfaces[name]) {
      debug("Loading interface '" + name + "'");
      app.interfaces[name] = interface(app);
      if(app.interfaces[name] && _.isFunction(app.interfaces[name].start)) {
        debug("Starting interface '" + name + "'");
        app.interfaces[name].start();
      }
    } else {
      debug("Not loading interface '" + name + "' because of configuration");
    }
  });
}

Server.prototype.reload = function(mixed) {
  var settings, self = this;

  if(typeof mixed === 'string') {
    try {
      settings = require(path.join(process.cwd(), mixed));
    } catch(e) {
      debug("Settings file '" + settings + "' doesn't exist.");
    }
  }

  if(mixed !== null && typeof mixed === 'object') {
    settings = mixed;
  }

  if(settings) {
    this.app.config.settings = settings;
  }

  this.stop();

  setTimeout(function() {
    self.start();
  }, 1000);

  return this;
};

Server.prototype.stop = function(cb) {
  if(this.app.started === true) {
    _.each(this.app.interfaces, function(intf) {
      if(intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') {
        intf.stop();
      }
    });

    debug("Closing server...");

    this.app.server.close(function() {
      debug("Server closed...");
      this.server = null;
      cb();
    });

    for(var id in this.app.connections) {
      if(this.app.connections.hasOwnProperty(id)) {
        debug("Closing connection #" + id);
        this.app.connections[id].destroy();
        delete this.app.connections[id];
      }
    }

    if(this.app.pruneInterval) {
      clearInterval(this.app.pruneInterval);
    }
    this.app.started = false;
  }

  this.app.providers.forEach(function(providerHolder) {
    providerHolder.pipeElements[0].end();
  });

  return this;
};
<|endoftext|>


<commit_before>'use strict';

const co = require('co');
const url = require('url');
const build = require('./build');
const State = require('./router').State;
const expand = require('./expand');
const HttpError = require('standard-http-error');

let objectify = function (route) {
  let r = typeof route !== 'object' ? { url: route } : route;
  r.url = r.url.toString();
  return r;
};
let extend = function (dest) {
  for (let src of Array.from(arguments).slice(1))
    for (let prop in src)
      if (src.hasOwnProperty(prop))
        dest[prop] = src[prop];
  return dest;
};

let makeState = function (req) {
  let urlParts = url.parse(req.url, true);
  let path     = (urlParts.pathname || '').split('/');
  let query    = Object.keys(urlParts.query).map(x => [x, urlParts.query[x]]);
  let fragment = urlParts.hash ? urlParts.hash.slice(1) : null;
  let method   = req.method;
  let headers  = Object.keys(req.headers).map(x => [x, req.headers[x]]);
  return new State(path, query, fragment, method, headers, []);
};
let mergeState = function (state, url, saved) {
  let newstate = makeState({
    url    : url.toString(),
    method : state.method,
    headers: state.headers
  });
  newstate.saved = saved || [];
  return newstate;
};

let promise = function (f) {
  return new Promise((resolve, reject) => f(err => err ? reject(err) : resolve()));
};

class App {

  constructor(opts, _parent) {
    this.opts   = opts || {};
    this.Context= this.opts.context;
    this.routes = [];
    this.parent = _parent || this;

    if (!_parent) {
      this.middleware = new App(this.opts, this);
      this.catch = new App(this.opts, this);
    }
  }

  on(route, middleware, handler) {
    if (this.routes === null)
      throw new Error('cannot add routes after router has been finalized');

    if (!handler) {
      handler = middleware;
      middleware = [];
    }

    middleware = middleware || [];

    expand(extend({method: '[.*]'}, objectify(route))).forEach(route => {
      this.routes.push(extend(route, {middleware, handler}));
    });

    return this;
  }

  get(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'GET'}), middleware, handler);
  }
  post(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'POST'}), middleware, handler);
  }
  put(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'PUT'}), middleware, handler);
  }
  del(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'DELETE'}), middleware, handler);
  }
  all(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: '[.*]'}), middleware, handler);
  }

  finalize() {
    if (this.routes === null)
      return this;

    if (this.middleware)
      this.middleware.finalize(this.opts);

    if (this.catch)
      this.catch.finalize(this.opts);

    this.router = build(this.routes, this.opts);
    this.routes = null;

    return this;
  }

  run(state) {
    return new Promise((resolve, reject) => {
      this.router.unRouter(state, (x,s) => {

        resolve((x.middleware || []).reduceRight((f, mw) => {
          if (typeof mw === 'function')
            return ctx => co(mw.call(ctx, f.bind(null, ctx)));

          return ctx => {
            let newstate = mergeState(state, mw, [f.bind(null, ctx)]);
            return this.parent.middleware.run(newstate).then(h => h(ctx));
          };

        }, ctx => co(x.handler.apply(ctx, s.saved))));

      }, () => reject(new HttpError(404)));
    });
  }
  dispatch(req, res) {
    let ctx   = this.Context ? new this.Context(req, res) : {req, res};
    let state = makeState(req);

    let errorHandlerNotFound = code => () => {
      throw new Error('No error handler for `' + state.method + ' ' + code + '`');
    };

    let errorHandler = err => {
      if (err instanceof HttpError)
        return this.parent.catch.run(mergeState(state, err.code))
          .catch(errorHandlerNotFound(err.code))
          .then(h => h(ctx))
          .catch(errorHandler);
      else
        return this.parent.catch.run(mergeState(state, 500, [err]))
          .catch(errorHandlerNotFound(500))
          .then(h => h(ctx));
    };

    return this.run(state).then(h => h(ctx)).catch(errorHandler);
  }

  start(opts) {
    this.finalize();

    if (!opts)
      throw new Error('missing options');

    if (!opts.http && !opts.https)
      throw new Error('unable to start server, missing server configuration');

    let cbs = [];

    if (opts.http) {
      this.httpServer = require('http').createServer((req, res) => this.dispatch(req, res));
      cbs.push(promise(cb => this.httpServer.listen(opts.http, cb)));
    }

    if (opts.https) {
      this.httpsServer = require('https').createServer(opts, (req, res) => this.dispatch(req, res));
      cbs.push(promise(cb => this.httpsServer.listen(opts.https, cb)));
    }

    return Promise.all(cbs);
  }
  stop() {
    let cbs = [];

    if (this.httpServer)
      cbs.push(promise(cb => this.httpServer.close(cb)));

    if (this.httpsServer)
      cbs.push(promise(cb => this.httpsServer.close(cb)));

    return Promise.all(cbs);
  }
}

module.exports = App;
module.exports.HttpError = HttpError;
<commit_msg>better control flow<commit_after>'use strict';

const co = require('co');
const url = require('url');
const build = require('./build');
const State = require('./router').State;
const expand = require('./expand');
const HttpError = require('standard-http-error');

let objectify = function (route) {
  let r = typeof route !== 'object' ? { url: route } : route;
  r.url = r.url.toString();
  return r;
};
let extend = function (dest) {
  for (let src of Array.from(arguments).slice(1))
    for (let prop in src)
      if (src.hasOwnProperty(prop))
        dest[prop] = src[prop];
  return dest;
};

let makeState = function (req) {
  let urlParts = url.parse(req.url, true);
  let path     = (urlParts.pathname || '').split('/');
  let query    = Object.keys(urlParts.query).map(x => [x, urlParts.query[x]]);
  let fragment = urlParts.hash ? urlParts.hash.slice(1) : null;
  let method   = req.method;
  let headers  = Object.keys(req.headers).map(x => [x, req.headers[x]]);
  return new State(path, query, fragment, method, headers, []);
};
let mergeState = function (state, url, saved) {
  let newstate = makeState({
    url    : url.toString(),
    method : state.method,
    headers: state.headers
  });
  newstate.saved = saved || [];
  return newstate;
};

let promise = function (f) {
  return new Promise((resolve, reject) => f(err => err ? reject(err) : resolve()));
};

class App {

  constructor(opts, _parent) {
    this.opts   = opts || {};
    this.Context= this.opts.context;
    this.routes = [];
    this.parent = _parent || this;
    this.global = [];

    if (!_parent) {
      this.middleware = new App(this.opts, this);
      this.catch = new App(this.opts, this);
    }
  }

  use(middleware) {
    this.global.push(middleware);
  }

  on(route, middleware, handler) {
    if (this.routes === null)
      throw new Error('cannot add routes after router has been finalized');

    if (!handler) {
      handler = middleware;
      middleware = [];
    }

    middleware = middleware || [];

    expand(extend({method: '[.*]'}, objectify(route))).forEach(route => {
      this.routes.push(extend(route, {middleware, handler}));
    });

    return this;
  }

  get(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'GET'}), middleware, handler);
  }
  post(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'POST'}), middleware, handler);
  }
  put(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'PUT'}), middleware, handler);
  }
  del(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: 'DELETE'}), middleware, handler);
  }
  all(route, middleware, handler) {
    return this.on(extend(objectify(route), {method: '[.*]'}), middleware, handler);
  }

  finalize() {
    if (this.routes === null)
      return this;

    if (this.middleware)
      this.middleware.finalize(this.opts);

    if (this.catch)
      this.catch.finalize(this.opts);

    this.router = build(this.routes, this.opts);
    this.routes = null;

    return this;
  }

  run(state) {
    return new Promise((resolve, reject) => {
      this.router.unRouter(state, (x,s) => {

        resolve(x.middleware.reduceRight((f, mw) => {
          if (typeof mw === 'function')
            return ctx => co(mw.call(ctx, f.bind(null, ctx)));

          return ctx => {
            let newstate = mergeState(state, mw, [f.bind(null, ctx)]);
            return this.parent.middleware.run(newstate)
              .catch(err => { throw new Error('No middleware handler found for `' + state.method + ' ' + mw + '`') })
              .then(h => h(ctx));
          };
        }, ctx => co(x.handler.apply(ctx, s.saved))));

      }, () => reject(new HttpError(404)));
    });
  }
  dispatch(req, res) {
    let ctx   = this.Context ? new this.Context(req, res) : {req, res};
    let state = makeState(req);

    let errorHandlerNotFound = code => () => {
      throw new Error('No error handler for `' + state.method + ' ' + code + '`');
    };

    let errorHandler = err => {
      if (err instanceof HttpError)
        return this.parent.catch.run(mergeState(state, err.code))
          .catch(errorHandlerNotFound(err.code))
          .then(h => h(ctx))
          .catch(errorHandler);
      else
        return this.parent.catch.run(mergeState(state, 500, [err]))
          .catch(errorHandlerNotFound(500))
          .then(h => h(ctx));
    };

    return this.global.reduceRight((f, mw) => {
      return ctx => co(mw.call(ctx, f.bind(null, ctx)));
    }, ctx => this.run(state).then(h => h(ctx)).catch(errorHandler))(ctx).catch(err => {
      if (!res.headersSent)
        res.statusCode = 500;
      if (!res.finished)
        res.end();
    });
  }

  start(opts) {
    this.finalize();

    if (!opts)
      throw new Error('missing options');

    if (!opts.http && !opts.https)
      throw new Error('unable to start server, missing server configuration');

    let cbs = [];

    if (opts.http) {
      this.httpServer = require('http').createServer((req, res) => this.dispatch(req, res));
      cbs.push(promise(cb => this.httpServer.listen(opts.http, cb)));
    }

    if (opts.https) {
      this.httpsServer = require('https').createServer(opts, (req, res) => this.dispatch(req, res));
      cbs.push(promise(cb => this.httpsServer.listen(opts.https, cb)));
    }

    return Promise.all(cbs);
  }
  stop() {
    let cbs = [];

    if (this.httpServer)
      cbs.push(promise(cb => this.httpServer.close(cb)));

    if (this.httpsServer)
      cbs.push(promise(cb => this.httpsServer.close(cb)));

    return Promise.all(cbs);
  }
}

module.exports = App;
module.exports.HttpError = HttpError;
<|endoftext|>


<commit_before>'use strict';

var dt = require('no-promise-dir-tree');
var base64File = require('base64-file-coder-node')();
var currentDir = process.cwd();
var path = require('path');
var each = require('async-each');

var rootname = 'index.html';
var rootFile;
var parseObject = function(obj, path){
  path = path || '';
  var response = [];
  for (var i in obj) {
    if (typeof obj[i] === 'object') {
      response = response.concat(parseObject(obj[i], path + i + '/'));
    } else if (obj[i] === true) {
      if (path+i === rootname) {
        rootFile = {
          path: path,
          name: i
        };
      } else {
        response.push({
          path: path,
          name: i
        });
      }
    }
  }

  return response;
};

var folders = {};
var fileTypes = [
  {
    type: 'image',
    ext: ['.png', '.jpg', '.gif'],
  },
  {
    type: 'css',
    ext: ['.css'],
  },
  {
    type: 'js',
    ext: ['.js'],
  }
];

var getFileType = function(name){
  var matchedType = fileTypes.filter(function(fileType){
    return fileType.ext.indexOf(path.extname(name)) > -1;
  });

  return matchedType[0].type;
};

dt(currentDir, function(err, fileTree){
  var files = parseObject(fileTree);
  each(files, function(file, cb) {
    base64File.encode(file.path + file.name, function(err, data) {
      cb(null, [file.path + file.name, data, getFileType(file.name)]);
    });
  }, function(err, content){
    for (var i in content) {
      folders[content[i][2]] = folders[content[i][2]] || {};
      folders[content[i][2]][content[i][0]] = content[i][1];
    }
    // END OF IMAGES COLLECTING
    console.log(JSON.stringify(folders));
  });
});
<commit_msg>:man: build the dest file<commit_after>'use strict';

var dt = require('no-promise-dir-tree');
var base64File = require('base64-file-coder-node')();
var currentDir = process.cwd();
var path = require('path');
var fs = require('fs');
var filendir = require('filendir');
var cheerio = require('cheerio');
var each = require('async-each');

var rootname = 'index.html';
var rootFile;
var parseObject = function(obj, path){
  path = path || '';
  var response = [];
  for (var i in obj) {
    if (typeof obj[i] === 'object') {
      response = response.concat(parseObject(obj[i], path + i + '/'));
    } else if (obj[i] === true) {
      if (path+i === rootname) {
        rootFile = {
          path: path,
          name: i
        };
      } else {
        response.push({
          path: path,
          name: i
        });
      }
    }
  }

  return response;
};

var folders = {};
var fileTypes = [
  {
    type: 'image',
    ext: ['.png', '.jpg', '.gif'],
  },
  {
    type: 'css',
    ext: ['.css'],
  },
  {
    type: 'js',
    ext: ['.js'],
  }
];

var getFileType = function(name){
  var matchedType = fileTypes.filter(function(fileType){
    return fileType.ext.indexOf(path.extname(name)) > -1;
  });

  return matchedType[0].type;
};

var mimeTypes = {
  'image': 'image/png',
  'js': 'text/javascript',
  'css': 'text/css'
};

dt(currentDir, function(err, fileTree){
  var files = parseObject(fileTree);
  each(files, function(file, cb) {
    base64File.encode(file.path + file.name, function(err, data) {
      cb(null, [file.path + file.name, data, getFileType(file.name)]);
    });
  }, function(err, content){
    for (var i in content) {
      folders[content[i][2]] = folders[content[i][2]] || {};
      folders[content[i][2]][content[i][0]] =
        'data:' + mimeTypes[folders[content[i][2]]] + ';base64,' +
        content[i][1];
    }
    // END OF FILES COLLECTING
    //console.log(JSON.stringify(folders));
    fs.readFile(rootname, {encoding: 'utf-8'}, function(err,data) {
      if (!err){
        var $ = cheerio.load(data);
        fs.readFile(__dirname + '/starter.js', {encoding: 'utf-8'},
          function(err, starter) {
            $('body').append(
              '<script>' +
                'var folders = ' + JSON.stringify(folders) + ';\n' +
                starter +
              '</script>'
            );

            filendir.wa('expac/' + rootname, $.html(), function(err) {
              if(err) {
                return console.log(err);
              }

              console.log('The file was saved as expac/' + rootname);
            });

          }
        );
      }else{
        console.log(err);
      }
    });
  });
});
<|endoftext|>