<commit_before>'use strict';

require('arraybuffer-slice');

var
    asmCrypto = require('asmcrypto.js'),
    pako = require('pako'),
    xmldom = require('xmldom'),

    KdbxError = require('./../errors/kdbx-error'),
    KdbxCredentials = require('./kdbx-credentials'),
    KdbxHeader = require('./kdbx-header'),
    KdbxMeta = require('./kdbx-meta'),
    KdbxGroup = require('./kdbx-group'),
    KdbxDeletedObject = require('./kdbx-deleted-object'),
    BinaryStream = require('./../utils/binary-stream'),
    Consts = require('./../defs/consts'),
    XmlNames = require('./../defs/xml-names'),
    HashedBlockTransform = require('./../crypto/hashed-block-transform'),
    ByteUtils = require('./../utils/byte-utils'),
    XmlUtils = require('./../utils/xml-utils'),
    ProtectSaltGenerator = require('./../crypto/protect-salt-generator'),
    Random = require('./../crypto/random');

/**
 * Kdbx file (KeePass database v2)
 * @constructor
 */
var Kdbx = function() {
};

/**
 * Load kdbx file
 * If there was an error loading file, throws an exception
 * @param {ArrayBuffer} data - database file contents
 * @param {KdbxCredentials} credentials
 * @return {Kdbx} - loaded Kdbx file
 */
Kdbx.load = function(data, credentials) {
    if (!(data instanceof ArrayBuffer)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidArg, 'data');
    }
    if (!(credentials instanceof KdbxCredentials)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidArg, 'credentials');
    }
    var stm = new BinaryStream(data);
    var kdbx = new Kdbx(credentials);
    kdbx.credentials = credentials;
    kdbx._readHeader(stm);
    kdbx._decryptXml(stm);
    kdbx._setProtectedValues();
    kdbx._loadFromXml();
    kdbx._checkHeaderHash(stm);
    return kdbx;
};

/**
 * Save db to ArrayBuffer
 * @return {ArrayBuffer} - database file contents
 */
Kdbx.prototype.save = function() {
    var stm = new BinaryStream();
    this._generateSalts();
    this._buildXml();
    this._updateProtectedValuesSalt();
    this._encryptXml();
    this._setHeaderHash(stm);
    this._writeHeader(stm);
    return stm.getWrittenBytes();
};

Kdbx.prototype._generateSalts = function() {
    this.header.masterSeed = Random.getBytes(32);
    this.header.transformSeed = Random.getBytes(32);
    this.header.encryptionIV = Random.getBytes(16);
    this.header.protectedStreamKey = Random.getBytes(32);
    this.header.streamStartBytes = Random.getBytes(32);
};

Kdbx.prototype._readHeader = function(stm) {
    this.header = KdbxHeader.read(stm);
};

Kdbx.prototype._writeHeader = function(stm) {
    this.header.write(stm);
};

Kdbx.prototype._getMasterKey = function() {
    var credHash = this.credentials.hash.getBinary(),
        key = credHash,
        transformSeed = this.header.transformSeed,
        transformRounds = this.header.keyEncryptionRounds,
        aes = asmCrypto.AES_ECB;
    while (transformRounds--) {
        key = aes.encrypt(key, transformSeed, false, null);
    }
    ByteUtils.zeroBuffer(credHash);
    var keyHash = asmCrypto.SHA256.bytes(key);
    ByteUtils.zeroBuffer(key);
    var sha256 = new asmCrypto.SHA256().reset();
    var masterKey = ByteUtils.arrayToBuffer(sha256.process(this.header.masterSeed).process(keyHash).finish().result);
    ByteUtils.zeroBuffer(keyHash);
    return masterKey;
};

Kdbx.prototype._decryptXml = function(stm) {
    var data = stm.readBytesToEnd();
    var masterKey = this._getMasterKey();
    data = ByteUtils.arrayToBuffer(asmCrypto.AES_CBC.decrypt(data, masterKey, false, this.header.encryptionIV));
    ByteUtils.zeroBuffer(masterKey);

    data = this._trimStartBytes(data);

    data = HashedBlockTransform.decrypt(data);

    if (this.header.compression === Consts.CompressionAlgorithm.GZip) {
        data = pako.ungzip(data);
    }

    var xml = ByteUtils.bytesToString(data);
    this._parseXml(xml);
};

Kdbx.prototype._encryptXml = function() {
    var xml = this._serializeXml();
    var data = ByteUtils.arrayToBuffer(ByteUtils.stringToBytes(xml));
    if (this.header.compression === Consts.CompressionAlgorithm.GZip) {
        data = pako.gzip(data);
    }

    data = HashedBlockTransform.encrypt(ByteUtils.arrayToBuffer(data));

    var ssb = new Uint8Array(this.header.streamStartBytes);
    var newData = new Uint8Array(data.byteLength + ssb.length);
    newData.set(ssb);
    newData.set(new Uint8Array(data), ssb.length);
    data = newData;

    var masterKey = this._getMasterKey();
    data = ByteUtils.arrayToBuffer(asmCrypto.AES_CBC.encrypt(data, masterKey, true, this.header.encryptionIV));
    ByteUtils.zeroBuffer(masterKey);
    return data;
};

Kdbx.prototype._trimStartBytes = function(data) {
    var ssb = this.header.streamStartBytes;
    if (data.byteLength < ssb.byteLength) {
        throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'short start bytes');
    }
    if (!ByteUtils.arrayBufferEquals(data.slice(0, this.header.streamStartBytes.byteLength), ssb)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidKey);
    }
    return data.slice(ssb.byteLength);
};

Kdbx.prototype._checkHeaderHash = function(stm) {
    if (this.meta.headerHash) {
        var metaHash = this.meta.headerHash;
        var actualHash = this._getHeaderHash(stm);
        if (!ByteUtils.arrayBufferEquals(metaHash, actualHash)) {
            throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'header hash mismatch');
        }
    }
};

Kdbx.prototype._setHeaderHash = function(stm) {
    this.meta.headerHash = this._getHeaderHash(stm);
};

Kdbx.prototype._getHeaderHash = function(stm) {
    return ByteUtils.arrayToBuffer(asmCrypto.SHA256.bytes(stm.readBytesNoAdvance(0, this.header.endPos)));
};

Kdbx.prototype._parseXml = function(xml) {
    var DOMParser = xmldom.DOMParser;
    xml = new DOMParser().parseFromString(xml);
    Object.defineProperty(this, 'xml', { value: xml });
};

Kdbx.prototype._serializeXml = function() {
    var XMLSerializer = xmldom.XMLSerializer;
    return new XMLSerializer().serializeToString(this.xml);
};

Kdbx.prototype._setProtectedValues = function() {
    XmlUtils.setProtectedValues(this.xml.documentElement, this._getProtectSaltGenerator());
};

Kdbx.prototype._updateProtectedValuesSalt = function() {
    XmlUtils.updateProtectedValuesSalt(this.xml.documentElement, this._getProtectSaltGenerator());
};

Kdbx.prototype._getProtectSaltGenerator = function() {
    return new ProtectSaltGenerator(this.header.protectedStreamKey);
};

Kdbx.prototype._loadFromXml = function() {
    var doc = this.xml.documentElement;
    if (doc.tagName !== XmlNames.Elem.DocNode) {
        throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'bad xml root');
    }
    this._parseMeta();
    this._parseRoot();
};

Kdbx.prototype._parseMeta = function() {
    var node = XmlUtils.getChildNode(this.xml.documentElement, XmlNames.Elem.Meta, 'no meta node');
    this.meta = KdbxMeta.read(node);
};

Kdbx.prototype._parseRoot = function() {
    this.groups = [];
    this.deletedObjects = [];
    var node = XmlUtils.getChildNode(this.xml.documentElement, XmlNames.Elem.Root, 'no root node');
    for (var i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
        var childNode = cn[i];
        switch (childNode.tagName) {
            case XmlNames.Elem.Group:
                this._readGroup(childNode);
                break;
            case XmlNames.Elem.DeletedObjects:
                this._readDeletedObjects(childNode);
                break;
        }
    }
};

Kdbx.prototype._readDeletedObjects = function(node) {
    for (var i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
        var childNode = cn[i];
        switch (childNode.tagName) {
            case XmlNames.Elem.DeletedObject:
                this.deletedObjects.push(KdbxDeletedObject.read(childNode));
                break;
        }
    }
};

Kdbx.prototype._readGroup = function(node) {
    this.groups.push(KdbxGroup.read(node));
};

Kdbx.prototype._buildXml = function() {
    // TODO: build xml from saved state
};

module.exports = Kdbx;
<commit_msg>fix data writing<commit_after>'use strict';

require('arraybuffer-slice');

var
    asmCrypto = require('asmcrypto.js'),
    pako = require('pako'),
    xmldom = require('xmldom'),

    KdbxError = require('./../errors/kdbx-error'),
    KdbxCredentials = require('./kdbx-credentials'),
    KdbxHeader = require('./kdbx-header'),
    KdbxMeta = require('./kdbx-meta'),
    KdbxGroup = require('./kdbx-group'),
    KdbxDeletedObject = require('./kdbx-deleted-object'),
    BinaryStream = require('./../utils/binary-stream'),
    Consts = require('./../defs/consts'),
    XmlNames = require('./../defs/xml-names'),
    HashedBlockTransform = require('./../crypto/hashed-block-transform'),
    ByteUtils = require('./../utils/byte-utils'),
    XmlUtils = require('./../utils/xml-utils'),
    ProtectSaltGenerator = require('./../crypto/protect-salt-generator'),
    Random = require('./../crypto/random');

/**
 * Kdbx file (KeePass database v2)
 * @constructor
 */
var Kdbx = function() {
};

/**
 * Load kdbx file
 * If there was an error loading file, throws an exception
 * @param {ArrayBuffer} data - database file contents
 * @param {KdbxCredentials} credentials
 * @return {Kdbx} - loaded Kdbx file
 */
Kdbx.load = function(data, credentials) {
    if (!(data instanceof ArrayBuffer)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidArg, 'data');
    }
    if (!(credentials instanceof KdbxCredentials)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidArg, 'credentials');
    }
    var stm = new BinaryStream(data);
    var kdbx = new Kdbx(credentials);
    kdbx.credentials = credentials;
    kdbx._readHeader(stm);
    kdbx._decryptXml(stm);
    kdbx._setProtectedValues();
    kdbx._loadFromXml();
    kdbx._checkHeaderHash(stm);
    return kdbx;
};

/**
 * Save db to ArrayBuffer
 * @return {ArrayBuffer} - database file contents
 */
Kdbx.prototype.save = function() {
    var stm = new BinaryStream();
    this._generateSalts();
    this._buildXml();
    this._updateProtectedValuesSalt();
    var data = this._encryptXml();
    this._setHeaderHash(stm);
    this._writeHeader(stm);
    stm.writeBytes(data);
    return stm.getWrittenBytes();
};

Kdbx.prototype._generateSalts = function() {
    this.header.masterSeed = Random.getBytes(32);
    this.header.transformSeed = Random.getBytes(32);
    this.header.encryptionIV = Random.getBytes(16);
    this.header.protectedStreamKey = Random.getBytes(32);
    this.header.streamStartBytes = Random.getBytes(32);
};

Kdbx.prototype._readHeader = function(stm) {
    this.header = KdbxHeader.read(stm);
};

Kdbx.prototype._writeHeader = function(stm) {
    this.header.write(stm);
};

Kdbx.prototype._getMasterKey = function() {
    var credHash = this.credentials.hash.getBinary(),
        key = credHash,
        transformSeed = this.header.transformSeed,
        transformRounds = this.header.keyEncryptionRounds,
        aes = asmCrypto.AES_ECB;
    while (transformRounds--) {
        key = aes.encrypt(key, transformSeed, false, null);
    }
    ByteUtils.zeroBuffer(credHash);
    var keyHash = asmCrypto.SHA256.bytes(key);
    ByteUtils.zeroBuffer(key);
    var sha256 = new asmCrypto.SHA256().reset();
    var masterKey = ByteUtils.arrayToBuffer(sha256.process(this.header.masterSeed).process(keyHash).finish().result);
    ByteUtils.zeroBuffer(keyHash);
    return masterKey;
};

Kdbx.prototype._decryptXml = function(stm) {
    var data = stm.readBytesToEnd();
    var masterKey = this._getMasterKey();
    data = ByteUtils.arrayToBuffer(asmCrypto.AES_CBC.decrypt(data, masterKey, false, this.header.encryptionIV));
    ByteUtils.zeroBuffer(masterKey);

    data = this._trimStartBytes(data);

    data = HashedBlockTransform.decrypt(data);

    if (this.header.compression === Consts.CompressionAlgorithm.GZip) {
        data = pako.ungzip(data);
    }

    var xml = ByteUtils.bytesToString(data);
    this._parseXml(xml);
};

Kdbx.prototype._encryptXml = function() {
    var xml = this._serializeXml();
    var data = ByteUtils.arrayToBuffer(ByteUtils.stringToBytes(xml));
    if (this.header.compression === Consts.CompressionAlgorithm.GZip) {
        data = pako.gzip(data);
    }

    data = HashedBlockTransform.encrypt(ByteUtils.arrayToBuffer(data));

    var ssb = new Uint8Array(this.header.streamStartBytes);
    var newData = new Uint8Array(data.byteLength + ssb.length);
    newData.set(ssb);
    newData.set(new Uint8Array(data), ssb.length);
    data = newData;

    var masterKey = this._getMasterKey();
    data = ByteUtils.arrayToBuffer(asmCrypto.AES_CBC.encrypt(data, masterKey, true, this.header.encryptionIV));
    ByteUtils.zeroBuffer(masterKey);
    return data;
};

Kdbx.prototype._trimStartBytes = function(data) {
    var ssb = this.header.streamStartBytes;
    if (data.byteLength < ssb.byteLength) {
        throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'short start bytes');
    }
    if (!ByteUtils.arrayBufferEquals(data.slice(0, this.header.streamStartBytes.byteLength), ssb)) {
        throw new KdbxError(Consts.ErrorCodes.InvalidKey);
    }
    return data.slice(ssb.byteLength);
};

Kdbx.prototype._checkHeaderHash = function(stm) {
    if (this.meta.headerHash) {
        var metaHash = this.meta.headerHash;
        var actualHash = this._getHeaderHash(stm);
        if (!ByteUtils.arrayBufferEquals(metaHash, actualHash)) {
            throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'header hash mismatch');
        }
    }
};

Kdbx.prototype._setHeaderHash = function(stm) {
    this.meta.headerHash = this._getHeaderHash(stm);
};

Kdbx.prototype._getHeaderHash = function(stm) {
    return ByteUtils.arrayToBuffer(asmCrypto.SHA256.bytes(stm.readBytesNoAdvance(0, this.header.endPos)));
};

Kdbx.prototype._parseXml = function(xml) {
    var DOMParser = xmldom.DOMParser;
    xml = new DOMParser().parseFromString(xml);
    Object.defineProperty(this, 'xml', { value: xml });
};

Kdbx.prototype._serializeXml = function() {
    var XMLSerializer = xmldom.XMLSerializer;
    return new XMLSerializer().serializeToString(this.xml);
};

Kdbx.prototype._setProtectedValues = function() {
    XmlUtils.setProtectedValues(this.xml.documentElement, this._getProtectSaltGenerator());
};

Kdbx.prototype._updateProtectedValuesSalt = function() {
    XmlUtils.updateProtectedValuesSalt(this.xml.documentElement, this._getProtectSaltGenerator());
};

Kdbx.prototype._getProtectSaltGenerator = function() {
    return new ProtectSaltGenerator(this.header.protectedStreamKey);
};

Kdbx.prototype._loadFromXml = function() {
    var doc = this.xml.documentElement;
    if (doc.tagName !== XmlNames.Elem.DocNode) {
        throw new KdbxError(Consts.ErrorCodes.FileCorrupt, 'bad xml root');
    }
    this._parseMeta();
    this._parseRoot();
};

Kdbx.prototype._parseMeta = function() {
    var node = XmlUtils.getChildNode(this.xml.documentElement, XmlNames.Elem.Meta, 'no meta node');
    this.meta = KdbxMeta.read(node);
};

Kdbx.prototype._parseRoot = function() {
    this.groups = [];
    this.deletedObjects = [];
    var node = XmlUtils.getChildNode(this.xml.documentElement, XmlNames.Elem.Root, 'no root node');
    for (var i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
        var childNode = cn[i];
        switch (childNode.tagName) {
            case XmlNames.Elem.Group:
                this._readGroup(childNode);
                break;
            case XmlNames.Elem.DeletedObjects:
                this._readDeletedObjects(childNode);
                break;
        }
    }
};

Kdbx.prototype._readDeletedObjects = function(node) {
    for (var i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
        var childNode = cn[i];
        switch (childNode.tagName) {
            case XmlNames.Elem.DeletedObject:
                this.deletedObjects.push(KdbxDeletedObject.read(childNode));
                break;
        }
    }
};

Kdbx.prototype._readGroup = function(node) {
    this.groups.push(KdbxGroup.read(node));
};

Kdbx.prototype._buildXml = function() {
    // TODO: build xml from saved state
};

module.exports = Kdbx;
<|endoftext|>


<commit_before>var GithubApi = require('github'),
    config = require('../config'),
    Promise = require('promise'),
    _ = require('underscore'),
    github = new GithubApi({
      debug: config.debug,
      version: '3.0.0'
    }),
    debug = require('debug')('pulldasher:github'),
    utils = require('./utils'),
    Pull = require('../models/pull'),
    Issue = require('../models/issue'),
    Comment = require('../models/comment'),
    Label = require('../models/label'),
    Status = require('../models/status'),
    Signature = require('../models/signature'),
    getLogin  = require('./get-user-login'),
    rateLimit = require('./rate-limit.js');

github.authenticate({
   type: 'oauth',
   token: config.github.token
});

function denodeify(func) {
   return rateLimit(Promise.denodeify(func));
}

var api = {};
var getPull                = denodeify(github.pullRequests.get);
var getAllPulls            = denodeify(github.pullRequests.getAll);
var getIssue               = denodeify(github.issues.getRepoIssue);
var getAllIssues           = denodeify(github.issues.repoIssues);
api.getIssueEvents         = denodeify(github.issues.getEvents);
// "issue" comments are the comments on the pull (issue) itself.
api.getIssueComments       = denodeify(github.issues.getComments);
// "review" comments are the comments on the diff of the pull
api.getPullReviewComments  = denodeify(github.pullRequests.getComments);
api.getCommit              = denodeify(github.repos.getCommits);
api.getCommitStatus        = denodeify(github.statuses.get);
var getNextPage            = denodeify(github.getNextPage.bind(github));

module.exports = {
   github: github,

   /**
    * Returns a promise which resolves to a GitHub API response to
    * a query for a particular Pull Request.
    */
   getPull: function(repo, number) {
      debug("Getting pull %s", number);
      return getPull(params({
         number:     number
      }, repo));
   },

   /**
    * Get all *open* pull requests for a repo.
    *
    * Returns a promise which resolves to an array of all open pull requests
    */
   getOpenPulls: function(repo) {
      debug("Getting open pulls");
      return getAllPulls(params({}, repo)).then(getAllPages);
   },

   /**
    * Get *all* pull requests for a repo.
    *
    * Returns a promise which resolves to an array of all pull requests
    */
   getAllPulls: function(repo) {
      debug("Getting all pulls");
      return getAllPulls(params({
         state: 'all'
      }, repo)).then(getAllPages);
   },

   /**
    * Get an issue for a repo.
    *
    * Returns a promise which resolves to a github issue
    */
   getIssue: function(repo, number) {
      var searchParams = params({number: number}, repo);
      debug("Getting issue %s", number);
      return getIssue(searchParams)
      .then(addRepo(searchParams));
   },

   /**
    * Get all open issues for a repo.
    *
    * Returns a promise which resolves to an array of all open issues
    */
   getOpenIssues: function(repo) {
      var searchParams = params({}, repo);
      debug("Getting open issues");
      return getAllIssues(searchParams)
      .then(getAllPages)
      .then(filterOutPulls)
      .then(addRepo(searchParams));
   },

   /**
    * Get *all* issues for a repo.
    *
    * Returns a promise which resolves to an array of all issues
    */
   getAllIssues: function(repo) {
      var searchParams = params({state: 'all'}, repo);
      debug("Getting all issues");
      return getAllIssues(searchParams)
      .then(getAllPages)
      .then(filterOutPulls)
      .then(addRepo(searchParams));
   },

   /**
    * Takes a promise that resolves to a GitHub pull request API response,
    * parses it, and returns a promise that resolves to a Pull objects.
    */
   parse: function(githubPull) {
      debug("Getting all information for pull %s", githubPull.number);
      // We've occasionally noticed a null pull body, so lets fix it upfront
      // before errors happen.
      githubPull.body = githubPull.body || '';

      var repo = githubPull.head.repo.full_name;

      var reviewComments = getPullReviewComments(repo, githubPull.number);
      var comments = getIssueComments(repo, githubPull.number);
      var headCommit = getCommit(repo, githubPull.head.sha);
      var commitStatus = getCommitStatus(repo, githubPull.head.sha);
      var events = getIssueEvents(repo, githubPull.number);
      // Only so we have the canonical list of labels.
      var ghIssue = module.exports.getIssue(repo, githubPull.number);

      // Returned to the map function. Each element of githubPulls maps to
      // a promise that resolves to a Pull.
      return Promise.all([reviewComments,
                          comments,
                          headCommit,
                          commitStatus,
                          events,
                          ghIssue])
       .then(function(results) {
         var reviewComments = results[0],
             comments       = results[1],
             headCommit     = results[2],
             commitStatus   = results[3],
             events         = results[4],
             ghIssue        = results[5];

         // Array of Signature objects.
         var signatures = comments.reduce(function(sigs, comment) {
            var commentSigs = Signature.parseComment(
             comment, githubPull.base.repo.full_name, githubPull.number);

            // Signoffs from before the most recent commit are no longer active.
            var headCommitDate = new Date(headCommit.commit.committer.date);
            commentSigs.forEach(function(signature) {
               if ((signature.data.type === 'CR' ||
                signature.data.type === 'QA') &&
                new Date(signature.data.created_at) < headCommitDate) {
                  signature.data.active = false;
               }
            });

            return sigs.concat(commentSigs);
         }, []);

         // Array of Comment objects.
         comments = comments.map(function(commentData) {
            commentData.number = githubPull.number;
            commentData.repo_name = githubPull.base.repo.name;
            commentData.repo = githubPull.base.repo.full_name;
            commentData.type = 'issue';

            return new Comment(commentData);
         });

         // Array of Comment objects.
         comments = comments.concat(reviewComments.map(function(commentData) {
            commentData.number = githubPull.number;
            commentData.repo_name = githubPull.base.repo.name;
            commentData.repo = githubPull.base.repo.full_name;
            commentData.type = 'review';

            return new Comment(commentData);
         }));

         // Status object.
         var status = null;
         if (commitStatus) {
            status = new Status({
               repo:          githubPull.base.repo.full_name,
               sha:           githubPull.head.sha,
               state:         commitStatus.state,
               description:   commitStatus.description,
               target_url:    commitStatus.target_url
            });
         }

         // Array of Label objects.
         var labels = getLabelsFromEvents(events, ghIssue);

         var pull = Pull.fromGithubApi(githubPull, signatures, comments, status, labels);
         return pull.syncToIssue();
      });
   },

   /**
    * Takes a GitHub issue API response
    * parses it, and returns a promise that resolves to an Issue object.
    */
   parseIssue: function(ghIssue) {
      debug("Getting all information for issue %s", ghIssue.number);
      return getIssueEvents(ghIssue.number)
      .then(function(events) {
         // Array of Label objects.
         // Note: using the repo name from the config for now until we support
         // multiple repos. The ghIssue object doesn't contain the repo name.
         var labels = getLabelsFromEvents(events, ghIssue);

         return Issue.getFromGH(ghIssue, labels);
      });
   },
};

/**
 * Traverses github's pagination to retrieve *all* the records.
 * Takes in the first page of results and returns a promise for *all* the
 * results.
 */
function getAllPages(currentPage, allResults) {
   debug("Got %s results", currentPage.length);
   return new Promise(function (resolve, reject) {
      allResults = allResults || [];
      allResults.push(currentPage);

      if (!github.hasNextPage(currentPage)) {
         debug("No more results");
         return resolve(_.flatten(allResults));
      }

      resolve(getNextPage(currentPage).then(function (nextPage) {
         return getAllPages(nextPage, allResults);
      }));
   });
}

/**
 * Get array of Label objects from complete list of a Issue's events.
 *
 * Note: ghIssue at this point has always come from one of the
 * get*Issues() commands and thus has been augmented with the
 * issue.repo property.
 */
function getLabelsFromEvents(events, ghIssue) {
   // until we remove this entirely, let's jsut get it the easy way.
   var repo = ghIssue.repo;

   debug("Extracting label assignments from %s issue events for #%s",
    events.length, ghIssue.number);

   // Narrow list to relevant labeled/unlabeled events.
   events = _.filter(events, function(event) {
      return event.event === 'labeled' || event.event === 'unlabeled';
   });

   debug("Found %s label events for #%s", events.length, ghIssue.number);

   // Build simple Event objects with all the info we care about.
   events = events.map(function(event) {
      return {
         type: event.event,
         name: event.label.name,
         user: getLogin(event.actor),
         created_at: utils.fromDateString(event.created_at)
      };
   });

   // Group label events by label name.
   var labels = _.groupBy(events, 'name');

   // Get a list of the most recent events for each label.
   labels = _.map(labels, function(events) {
      events = _.sortBy(events, 'created_at');
      return _.last(events);
   });

   labels = _.filter(labels, function(event) {
      return event.type === 'labeled';
   });

   debug("Found %s unique labels for #%s", labels.length, ghIssue.number);

   // If these are available, use them as the canonical source, only augmented
   // by the data from events. If a label is renamed, the events will retain
   // the old name but the list of labels on the issue itself will be correct.
   // So, if a label is renamed, we'll lose the labeler and the date.
   if (ghIssue.labels && ghIssue.labels.length) {
      debug("Using %s labels from the github issue", ghIssue.labels.length);
      // Includes labeller and a time from the events api
      var eventLabels = _.indexBy(labels, 'name');

      return ghIssue.labels.map(function(label) {
         var eventLabel = eventLabels[label.name];
         return new Label(
            {name: label.name},
            ghIssue.number,
            repoName,
            ghIssue.repo,
            eventLabel && eventLabel.user,
            eventLabel && eventLabel.created_at
         );
      });
   }

   // Construct Label objects.
   return labels.map(function(label) {
      return new Label(
         {name: label.name},
         ghIssue.number,
         repoName,
         ghIssue.repo,
         label.user,
         label.created_at
      );
   });
}

/**
 * Return the default api params merged with the overrides
 */
function params(apiParams, repo) {
   var repoArray = parseRepo(repo);
   var owner = repoArray[0];
   var repoName = repoArray[1];

   return _.extend({
      user:       owner,
      repo:       repoName,
      per_page:   100,
   }, apiParams);
}

/**
 * Returns a function that uses the search parameters to add the "repo"
 * property to all the results. When we ask for a list of open issues from the
 * API for a particular repo, those results don't have references to the repo
 * we asked about, so we have to inject them to normalize the structure of the
 * object.
 */
function addRepo(params) {
   function addRepositoryField(ghIssue) {
      ghIssue.repo = params.user + "/" + params.repo;
   }
   return function(results) {
      if (Array.isArray(results)) {
         results.forEach(addRepositoryField);
      } else {
         addRepositoryField(results);
      }
      return results;
   };
}

/**
 * Splits the repo into the owner and repo name.
 */
function parseRepo(repo) {
   return repo.split("/");
}

/**
 * Return a promise for all issue events for the given issue / pull
 */
function getIssueEvents(repo, issueNumber) {
   debug("Getting events for issue #%s", issueNumber);
   return api.getIssueEvents(params({
      number: issueNumber
   }, repo)).then(getAllPages);
}

function getIssueComments(repo, issueNumber) {
   debug("Getting comments for issue #%s", issueNumber);
   return api.getIssueComments(params({
      number: issueNumber
   }, repo)).then(getAllPages);
}

function getPullReviewComments(repo, pullNumber) {
   debug("Getting pull review comments for pull #%s", pullNumber);
   return api.getPullReviewComments(params({
      number: pullNumber
   }, repo)).then(getAllPages);
}

function getCommit(repo, sha) {
   return new Promise(function (resolve, reject){
      debug("Getting commit %s", sha);
      api.getCommit(params({
         sha: sha,
         per_page: 1
      }, repo)).then(function(commits) {
         if (commits.length === 1) {
            resolve(commits[0]);
         } else {
            return reject();
         }
      });
   });
}

function getCommitStatus(repo, sha) {
   return new Promise(function(resolve, reject) {
      debug("Getting commit status for %s", sha);
      api.getCommitStatus(params({
         sha: sha
      }, repo)).then(function(status) {
         if (status.length) {
            resolve(status[0]);
         } else {
            resolve(null);
         }
      });
   });
}

/**
 * Remove all entries that have the pull_request key set to something truthy
 */
function filterOutPulls(issues) {
   debug("Filtering out pulls from list of %s issues", issues.length);
   issues = _.filter(issues, function(issue) {
      return !issue.pull_request || !issue.pull_request.url;
   });
   debug("Filtered down to %s issues", issues.length);
   return issues;
}
<commit_msg>Remove repo name insertions<commit_after>var GithubApi = require('github'),
    config = require('../config'),
    Promise = require('promise'),
    _ = require('underscore'),
    github = new GithubApi({
      debug: config.debug,
      version: '3.0.0'
    }),
    debug = require('debug')('pulldasher:github'),
    utils = require('./utils'),
    Pull = require('../models/pull'),
    Issue = require('../models/issue'),
    Comment = require('../models/comment'),
    Label = require('../models/label'),
    Status = require('../models/status'),
    Signature = require('../models/signature'),
    getLogin  = require('./get-user-login'),
    rateLimit = require('./rate-limit.js');

github.authenticate({
   type: 'oauth',
   token: config.github.token
});

function denodeify(func) {
   return rateLimit(Promise.denodeify(func));
}

var api = {};
var getPull                = denodeify(github.pullRequests.get);
var getAllPulls            = denodeify(github.pullRequests.getAll);
var getIssue               = denodeify(github.issues.getRepoIssue);
var getAllIssues           = denodeify(github.issues.repoIssues);
api.getIssueEvents         = denodeify(github.issues.getEvents);
// "issue" comments are the comments on the pull (issue) itself.
api.getIssueComments       = denodeify(github.issues.getComments);
// "review" comments are the comments on the diff of the pull
api.getPullReviewComments  = denodeify(github.pullRequests.getComments);
api.getCommit              = denodeify(github.repos.getCommits);
api.getCommitStatus        = denodeify(github.statuses.get);
var getNextPage            = denodeify(github.getNextPage.bind(github));

module.exports = {
   github: github,

   /**
    * Returns a promise which resolves to a GitHub API response to
    * a query for a particular Pull Request.
    */
   getPull: function(repo, number) {
      debug("Getting pull %s", number);
      return getPull(params({
         number:     number
      }, repo));
   },

   /**
    * Get all *open* pull requests for a repo.
    *
    * Returns a promise which resolves to an array of all open pull requests
    */
   getOpenPulls: function(repo) {
      debug("Getting open pulls");
      return getAllPulls(params({}, repo)).then(getAllPages);
   },

   /**
    * Get *all* pull requests for a repo.
    *
    * Returns a promise which resolves to an array of all pull requests
    */
   getAllPulls: function(repo) {
      debug("Getting all pulls");
      return getAllPulls(params({
         state: 'all'
      }, repo)).then(getAllPages);
   },

   /**
    * Get an issue for a repo.
    *
    * Returns a promise which resolves to a github issue
    */
   getIssue: function(repo, number) {
      var searchParams = params({number: number}, repo);
      debug("Getting issue %s", number);
      return getIssue(searchParams)
      .then(addRepo(searchParams));
   },

   /**
    * Get all open issues for a repo.
    *
    * Returns a promise which resolves to an array of all open issues
    */
   getOpenIssues: function(repo) {
      var searchParams = params({}, repo);
      debug("Getting open issues");
      return getAllIssues(searchParams)
      .then(getAllPages)
      .then(filterOutPulls)
      .then(addRepo(searchParams));
   },

   /**
    * Get *all* issues for a repo.
    *
    * Returns a promise which resolves to an array of all issues
    */
   getAllIssues: function(repo) {
      var searchParams = params({state: 'all'}, repo);
      debug("Getting all issues");
      return getAllIssues(searchParams)
      .then(getAllPages)
      .then(filterOutPulls)
      .then(addRepo(searchParams));
   },

   /**
    * Takes a promise that resolves to a GitHub pull request API response,
    * parses it, and returns a promise that resolves to a Pull objects.
    */
   parse: function(githubPull) {
      debug("Getting all information for pull %s", githubPull.number);
      // We've occasionally noticed a null pull body, so lets fix it upfront
      // before errors happen.
      githubPull.body = githubPull.body || '';

      var repo = githubPull.head.repo.full_name;

      var reviewComments = getPullReviewComments(repo, githubPull.number);
      var comments = getIssueComments(repo, githubPull.number);
      var headCommit = getCommit(repo, githubPull.head.sha);
      var commitStatus = getCommitStatus(repo, githubPull.head.sha);
      var events = getIssueEvents(repo, githubPull.number);
      // Only so we have the canonical list of labels.
      var ghIssue = module.exports.getIssue(repo, githubPull.number);

      // Returned to the map function. Each element of githubPulls maps to
      // a promise that resolves to a Pull.
      return Promise.all([reviewComments,
                          comments,
                          headCommit,
                          commitStatus,
                          events,
                          ghIssue])
       .then(function(results) {
         var reviewComments = results[0],
             comments       = results[1],
             headCommit     = results[2],
             commitStatus   = results[3],
             events         = results[4],
             ghIssue        = results[5];

         // Array of Signature objects.
         var signatures = comments.reduce(function(sigs, comment) {
            var commentSigs = Signature.parseComment(
             comment, githubPull.base.repo.full_name, githubPull.number);

            // Signoffs from before the most recent commit are no longer active.
            var headCommitDate = new Date(headCommit.commit.committer.date);
            commentSigs.forEach(function(signature) {
               if ((signature.data.type === 'CR' ||
                signature.data.type === 'QA') &&
                new Date(signature.data.created_at) < headCommitDate) {
                  signature.data.active = false;
               }
            });

            return sigs.concat(commentSigs);
         }, []);

         // Array of Comment objects.
         comments = comments.map(function(commentData) {
            commentData.number = githubPull.number;
            commentData.repo_name = githubPull.base.repo.name;
            commentData.repo = githubPull.base.repo.full_name;
            commentData.type = 'issue';

            return new Comment(commentData);
         });

         // Array of Comment objects.
         comments = comments.concat(reviewComments.map(function(commentData) {
            commentData.number = githubPull.number;
            commentData.repo_name = githubPull.base.repo.name;
            commentData.repo = githubPull.base.repo.full_name;
            commentData.type = 'review';

            return new Comment(commentData);
         }));

         // Status object.
         var status = null;
         if (commitStatus) {
            status = new Status({
               repo:          githubPull.base.repo.full_name,
               sha:           githubPull.head.sha,
               state:         commitStatus.state,
               description:   commitStatus.description,
               target_url:    commitStatus.target_url
            });
         }

         // Array of Label objects.
         var labels = getLabelsFromEvents(events, ghIssue);

         var pull = Pull.fromGithubApi(githubPull, signatures, comments, status, labels);
         return pull.syncToIssue();
      });
   },

   /**
    * Takes a GitHub issue API response
    * parses it, and returns a promise that resolves to an Issue object.
    */
   parseIssue: function(ghIssue) {
      debug("Getting all information for issue %s", ghIssue.number);
      return getIssueEvents(ghIssue.number)
      .then(function(events) {
         // Array of Label objects.
         // Note: using the repo name from the config for now until we support
         // multiple repos. The ghIssue object doesn't contain the repo name.
         var labels = getLabelsFromEvents(events, ghIssue);

         return Issue.getFromGH(ghIssue, labels);
      });
   },
};

/**
 * Traverses github's pagination to retrieve *all* the records.
 * Takes in the first page of results and returns a promise for *all* the
 * results.
 */
function getAllPages(currentPage, allResults) {
   debug("Got %s results", currentPage.length);
   return new Promise(function (resolve, reject) {
      allResults = allResults || [];
      allResults.push(currentPage);

      if (!github.hasNextPage(currentPage)) {
         debug("No more results");
         return resolve(_.flatten(allResults));
      }

      resolve(getNextPage(currentPage).then(function (nextPage) {
         return getAllPages(nextPage, allResults);
      }));
   });
}

/**
 * Get array of Label objects from complete list of a Issue's events.
 *
 * Note: ghIssue at this point has always come from one of the
 * get*Issues() commands and thus has been augmented with the
 * issue.repo property.
 */
function getLabelsFromEvents(events, ghIssue) {
   debug("Extracting label assignments from %s issue events for #%s",
    events.length, ghIssue.number);

   // Narrow list to relevant labeled/unlabeled events.
   events = _.filter(events, function(event) {
      return event.event === 'labeled' || event.event === 'unlabeled';
   });

   debug("Found %s label events for #%s", events.length, ghIssue.number);

   // Build simple Event objects with all the info we care about.
   events = events.map(function(event) {
      return {
         type: event.event,
         name: event.label.name,
         user: getLogin(event.actor),
         created_at: utils.fromDateString(event.created_at)
      };
   });

   // Group label events by label name.
   var labels = _.groupBy(events, 'name');

   // Get a list of the most recent events for each label.
   labels = _.map(labels, function(events) {
      events = _.sortBy(events, 'created_at');
      return _.last(events);
   });

   labels = _.filter(labels, function(event) {
      return event.type === 'labeled';
   });

   debug("Found %s unique labels for #%s", labels.length, ghIssue.number);

   // If these are available, use them as the canonical source, only augmented
   // by the data from events. If a label is renamed, the events will retain
   // the old name but the list of labels on the issue itself will be correct.
   // So, if a label is renamed, we'll lose the labeler and the date.
   if (ghIssue.labels && ghIssue.labels.length) {
      debug("Using %s labels from the github issue", ghIssue.labels.length);
      // Includes labeller and a time from the events api
      var eventLabels = _.indexBy(labels, 'name');

      return ghIssue.labels.map(function(label) {
         var eventLabel = eventLabels[label.name];
         return new Label(
            {name: label.name},
            ghIssue.number,
            "", // TODO: Remove this when repo_name is removed.
            ghIssue.repo,
            eventLabel && eventLabel.user,
            eventLabel && eventLabel.created_at
         );
      });
   }

   // Construct Label objects.
   return labels.map(function(label) {
      return new Label(
         {name: label.name},
         ghIssue.number,
         "", // TODO: Remove this when repo_name is removed.
         ghIssue.repo,
         label.user,
         label.created_at
      );
   });
}

/**
 * Return the default api params merged with the overrides
 */
function params(apiParams, repo) {
   var repoArray = parseRepo(repo);
   var owner = repoArray[0];
   var repoName = repoArray[1];

   return _.extend({
      user:       owner,
      repo:       repoName,
      per_page:   100,
   }, apiParams);
}

/**
 * Returns a function that uses the search parameters to add the "repo"
 * property to all the results. When we ask for a list of open issues from the
 * API for a particular repo, those results don't have references to the repo
 * we asked about, so we have to inject them to normalize the structure of the
 * object.
 */
function addRepo(params) {
   function addRepositoryField(ghIssue) {
      ghIssue.repo = params.user + "/" + params.repo;
   }
   return function(results) {
      if (Array.isArray(results)) {
         results.forEach(addRepositoryField);
      } else {
         addRepositoryField(results);
      }
      return results;
   };
}

/**
 * Splits the repo into the owner and repo name.
 */
function parseRepo(repo) {
   return repo.split("/");
}

/**
 * Return a promise for all issue events for the given issue / pull
 */
function getIssueEvents(repo, issueNumber) {
   debug("Getting events for issue #%s", issueNumber);
   return api.getIssueEvents(params({
      number: issueNumber
   }, repo)).then(getAllPages);
}

function getIssueComments(repo, issueNumber) {
   debug("Getting comments for issue #%s", issueNumber);
   return api.getIssueComments(params({
      number: issueNumber
   }, repo)).then(getAllPages);
}

function getPullReviewComments(repo, pullNumber) {
   debug("Getting pull review comments for pull #%s", pullNumber);
   return api.getPullReviewComments(params({
      number: pullNumber
   }, repo)).then(getAllPages);
}

function getCommit(repo, sha) {
   return new Promise(function (resolve, reject){
      debug("Getting commit %s", sha);
      api.getCommit(params({
         sha: sha,
         per_page: 1
      }, repo)).then(function(commits) {
         if (commits.length === 1) {
            resolve(commits[0]);
         } else {
            return reject();
         }
      });
   });
}

function getCommitStatus(repo, sha) {
   return new Promise(function(resolve, reject) {
      debug("Getting commit status for %s", sha);
      api.getCommitStatus(params({
         sha: sha
      }, repo)).then(function(status) {
         if (status.length) {
            resolve(status[0]);
         } else {
            resolve(null);
         }
      });
   });
}

/**
 * Remove all entries that have the pull_request key set to something truthy
 */
function filterOutPulls(issues) {
   debug("Filtering out pulls from list of %s issues", issues.length);
   issues = _.filter(issues, function(issue) {
      return !issue.pull_request || !issue.pull_request.url;
   });
   debug("Filtered down to %s issues", issues.length);
   return issues;
}
<|endoftext|>


<commit_before>var request = require('request')

const getGithubToken = (installationId, jwt) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: 'https://api.github.com/installations/' + installationId + '/access_tokens',
        auth: {'bearer': jwt},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.machine-man-preview+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem with the github certificate.') }
        else { resolve(JSON.parse(body).token) }
      }
    )
  })
}

const getPullRequestByUrl = (pullUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: pullUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull requests.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const getPullRequestByBranch = (fullName, branchName, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: 'https://api.github.com/repos/' + fullName + '/pulls',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        var pulls = JSON.parse(body)
        var thePull = pulls.find((pull) => {
          return pull && pull.head && pull.head.ref === branchName
        })
        if (err) { reject('There was a problem getting pull requests.') }
        else { resolve(thePull) }
      }
    )
  })
}

const getPullComments = (commentsUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: commentsUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull request comments.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const deleteComment = (commentUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.delete(
      {
        url: commentUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem deleting a comment.') }
        else { resolve() }
      }
    )
  })
}

const deleteMultipleComments = (comments, ghToken) => {
  var promises = []
  comments.forEach((comment) => {
    promises.push(new Promise((resolve, reject) => {
      deleteComment(comment.url, ghToken).then((err) => {
        if (err) { reject('There was a problem deleting one of multiple comments.') }
        else { resolve() }
      })
    }))
  })
  return Promise.all(promises)
}

const commentOnPullRequest = (commentsUrl, message, ghToken) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: commentsUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        },
        json: {body: message}
      },
      (err, res, body) => {
        if (err) { reject('There was a problem commenting on pull request.') }
        else { resolve(body) }
      }
    )
  })
}

const getPullLabels = (issueUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: issueUrl + '/labels',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull request labels.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const addLabelToPullRequest = (issueUrl, label, ghToken) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: issueUrl + '/labels',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        },
        json: [label]
      },
      (err, res, body) => {
        if (err) { reject('There was a problem adding a label to a pull request.') }
        else { resolve() }
      }
    )
  })
}

const deleteLabelFromPullRequest = (issueUrl, label, ghToken) => {
  return new Promise((resolve, reject) => {
    request.delete(
      {
        url: issueUrl + '/labels/' + label,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem deleting a label.') }
        else { resolve() }
      }
    )
  })
}

module.exports = {
  getGithubToken,
  getPullRequestByUrl,
  getPullRequestByBranch,
  getPullComments,
  deleteComment,
  deleteMultipleComments,
  commentOnPullRequest,
  getPullLabels,
  addLabelToPullRequest,
  deleteLabelFromPullRequest
}
<commit_msg>Fix so closed pull request can be found<commit_after>var request = require('request')

const getGithubToken = (installationId, jwt) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: 'https://api.github.com/installations/' + installationId + '/access_tokens',
        auth: {'bearer': jwt},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.machine-man-preview+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem with the github certificate.') }
        else { resolve(JSON.parse(body).token) }
      }
    )
  })
}

const getPullRequestByUrl = (pullUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: pullUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull request by url.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const getPullRequestByBranch = (fullName, branchName, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: 'https://api.github.com/repos/' + fullName + '/pulls',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        },
        qs: {state: 'all', head: fullName.split('/')[0] + ':' + branchName}
      },
      (err, res, body) => {
        var pulls = JSON.parse(body)
        if (err) { reject('There was a problem getting pull request by branch name.') }
        else { resolve(pulls[0]) } // <-- assume the most recent one is the one we want (ordered by created_at)
      }
    )
  })
}

const getPullComments = (commentsUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: commentsUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull request comments.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const deleteComment = (commentUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.delete(
      {
        url: commentUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem deleting a comment.') }
        else { resolve() }
      }
    )
  })
}

const deleteMultipleComments = (comments, ghToken) => {
  var promises = []
  comments.forEach((comment) => {
    promises.push(new Promise((resolve, reject) => {
      deleteComment(comment.url, ghToken).then((err) => {
        if (err) { reject('There was a problem deleting one of multiple comments.') }
        else { resolve() }
      })
    }))
  })
  return Promise.all(promises)
}

const commentOnPullRequest = (commentsUrl, message, ghToken) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: commentsUrl,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        },
        json: {body: message}
      },
      (err, res, body) => {
        if (err) { reject('There was a problem commenting on pull request.') }
        else { resolve(body) }
      }
    )
  })
}

const getPullLabels = (issueUrl, ghToken) => {
  return new Promise((resolve, reject) => {
    request.get(
      {
        url: issueUrl + '/labels',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem getting pull request labels.') }
        else { resolve(JSON.parse(body)) }
      }
    )
  })
}

const addLabelToPullRequest = (issueUrl, label, ghToken) => {
  return new Promise((resolve, reject) => {
    request.post(
      {
        url: issueUrl + '/labels',
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        },
        json: [label]
      },
      (err, res, body) => {
        if (err) { reject('There was a problem adding a label to a pull request.') }
        else { resolve() }
      }
    )
  })
}

const deleteLabelFromPullRequest = (issueUrl, label, ghToken) => {
  return new Promise((resolve, reject) => {
    request.delete(
      {
        url: issueUrl + '/labels/' + label,
        auth: {'bearer': ghToken},
        headers: {
          'User-Agent': 'node.js',
          'Accept': 'application/vnd.github.v3+json'
        }
      },
      (err, res, body) => {
        if (err) { reject('There was a problem deleting a label.') }
        else { resolve() }
      }
    )
  })
}

module.exports = {
  getGithubToken,
  getPullRequestByUrl,
  getPullRequestByBranch,
  getPullComments,
  deleteComment,
  deleteMultipleComments,
  commentOnPullRequest,
  getPullLabels,
  addLabelToPullRequest,
  deleteLabelFromPullRequest
}
<|endoftext|>


<commit_before>var fs = require('fs');
var shell = require('shelljs');
var grunt = require('grunt');
var spawn = require('child_process').spawn;
var version;

module.exports = {

  init: function() {
    if (!process.env.TRAVIS) {
      shell.exec('npm install');
    }
  },

  getVersion: function(){
    if (version) return version;

    var package = JSON.parse(fs.readFileSync('package.json', 'UTF-8'));
    var match = package.version.match(/^([^\-]*)(-snapshot)?$/);
    var semver = match[1].split('.');
    var hash = shell.exec('git rev-parse --short HEAD', {silent: true}).output.replace('\n', '');

    var fullVersion = (match[1] + (match[2] ? '-' + hash : ''));
    version = {
      full: fullVersion,
      major: semver[0],
      minor: semver[1],
      dot: semver[2].replace(/rc\d+/, ''),
      codename: package.codename,
      cdn: package.cdnVersion
    };

    return version;
  },

  startKarma: function(config, singleRun, done){
    var browsers = grunt.option('browsers');
    var reporters = grunt.option('reporters');
    var noColor = grunt.option('no-colors');
    var port = grunt.option('port');
    var p = spawn('node', ['node_modules/karma/bin/karma', 'start', config,
      singleRun ? '--single-run=true' : '',
      reporters ? '--reporters=' + reporters : '',
      browsers ? '--browsers=' + browsers : '',
      noColor ? '--no-colors' : '',
      port ? '--port=' + port : ''
    ]);
    p.stdout.pipe(process.stdout);
    p.stderr.pipe(process.stderr);
    p.on('exit', function(code){
      if(code !== 0) grunt.fail.warn("Karma test(s) failed. Exit code: " + code);
      done();
    });
  },

  wrap: function(src, name){
    src.unshift('src/' + name + '.prefix');
    src.push('src/' + name + '.suffix');
    return src;
  },

  addStyle: function(src, styles, minify){
    styles = styles.map(processCSS.bind(this)).join('\n');
    src += styles;
    return src;

    function processCSS(file){
      var css = fs.readFileSync(file).toString();
      if(minify){
        css = css
          .replace(/\r?\n/g, '')
          .replace(/\/\*.*?\*\//g, '')
          .replace(/:\s+/g, ':')
          .replace(/\s*\{\s*/g, '{')
          .replace(/\s*\}\s*/g, '}')
          .replace(/\s*\,\s*/g, ',')
          .replace(/\s*\;\s*/g, ';');
      }
      //escape for js
      css = css
        .replace(/\\/g, '\\\\')
        .replace(/'/g, "\\'")
        .replace(/\r?\n/g, '\\n');
      return "angular.element(document).find('head').prepend('<style type=\"text/css\">" + css + "</style>');";
    }
  },

  process: function(src, NG_VERSION, strict){
    var processed = src
      .replace(/"NG_VERSION_FULL"/g, NG_VERSION.full)
      .replace(/"NG_VERSION_MAJOR"/, NG_VERSION.major)
      .replace(/"NG_VERSION_MINOR"/, NG_VERSION.minor)
      .replace(/"NG_VERSION_DOT"/, NG_VERSION.dot)
      .replace(/"NG_VERSION_CDN"/, NG_VERSION.cdn)
      .replace(/"NG_VERSION_CODENAME"/, NG_VERSION.codename);
    if (strict !== false) processed = this.singleStrict(processed, '\n\n', true);
    return processed;
  },

  build: function(config, fn){
    var files = grunt.file.expand(config.src);
    var styles = config.styles;
    //concat
    var src = files.map(function(filepath){
      return grunt.file.read(filepath);
    }).join(grunt.util.normalizelf('\n'));
    //process
    var processed = this.process(src, grunt.config('NG_VERSION'), config.strict);
    if (styles) processed = this.addStyle(processed, styles.css, styles.minify);
    //write
    grunt.file.write(config.dest, processed);
    grunt.log.ok('File ' + config.dest + ' created.');
    fn();
  },

  singleStrict: function(src, insert){
    return src
      .replace(/\s*("|')use strict("|');\s*/g, insert) // remove all file-specific strict mode flags
      .replace(/(\(function\([^)]*\)\s*\{)/, "$1'use strict';"); // add single strict mode flag
  },

  sourceMap: function(mapFile, fileContents) {
    // use the following once Chrome beta or stable supports the //# pragma
    // var sourceMapLine = '//# sourceMappingURL=' + mapFile + '\n';
    var sourceMapLine = '/*\n//@ sourceMappingURL=' + mapFile + '\n*/\n';
    return fileContents + sourceMapLine;
  },

  min: function(file, done) {
    var classPathSep = (process.platform === "win32") ? ';' : ':';
    var minFile = file.replace(/\.js$/, '.min.js');
    var mapFile = minFile + '.map';
    var mapFileName = mapFile.match(/[^\/]+$/)[0];
    var errorFileName = file.replace(/\.js$/, '-errors.json');
    var versionNumber = this.getVersion().number;
    shell.exec(
        'java ' +
            this.java32flags() + ' ' +
            '-Xmx2g ' +
            '-cp bower_components/closure-compiler/compiler.jar' + classPathSep +
            'bower_components/ng-closure-runner/ngcompiler.jar ' +
            'org.angularjs.closurerunner.NgClosureRunner ' +
            '--compilation_level SIMPLE_OPTIMIZATIONS ' +
            '--language_in ECMASCRIPT5_STRICT ' +
            '--minerr_pass ' +
            '--minerr_errors ' + errorFileName + ' ' +
            '--minerr_url http://errors.angularjs.org/' + versionNumber + '/ ' +
            '--source_map_format=V3 ' +
            '--create_source_map ' + mapFile + ' ' +
            '--js ' + file + ' ' +
            '--js_output_file ' + minFile,
      function(code) {
        if (code !== 0) grunt.fail.warn('Error minifying ' + file);

        // closure creates the source map relative to build/ folder, we need to strip those references
        grunt.file.write(mapFile, grunt.file.read(mapFile).replace('"file":"build/', '"file":"').
                                                           replace('"sources":["build/','"sources":["'));

        // move add use strict into the closure + add source map pragma
        grunt.file.write(minFile, this.sourceMap(mapFileName, this.singleStrict(grunt.file.read(minFile), '\n')));
        grunt.log.ok(file + ' minified into ' + minFile);
        done();
    }.bind(this));
  },

  //returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
  java32flags: function(){
    if (process.platform === "win32") return '';
    if (shell.exec('java -version -d32 2>&1', {silent: true}).code !== 0) return '';
    return ' -d32 -client';
  },

  //collects and combines error messages stripped out in minify step
  collectErrors: function () {
    var combined = {
      id: 'ng',
      generated: new Date().toString(),
      errors: {}
    };
    grunt.file.expand('build/*-errors.json').forEach(function (file) {
      var errors = grunt.file.readJSON(file),
        namespace;
      Object.keys(errors).forEach(function (prop) {
        if (typeof errors[prop] === 'object') {
          namespace = errors[prop];
          if (combined.errors[prop]) {
            Object.keys(namespace).forEach(function (code) {
              if (combined.errors[prop][code] && combined.errors[prop][code] !== namespace[code]) {
                grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
              } else {
                combined.errors[prop][code] = namespace[code];
              }
            });
          } else {
            combined.errors[prop] = namespace;
          }
        } else {
          if (combined.errors[prop] && combined.errors[prop] !== errors[prop]) {
            grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
          } else {
            combined.errors[prop] = errors[prop];
          }
        }
      });
    });
    grunt.file.write('build/errors.json', JSON.stringify(combined));
    grunt.file.expand('build/*-errors.json').forEach(grunt.file.delete);
  },

  //csp connect middleware
  csp: function(){
    return function(req, res, next){
      res.setHeader("X-WebKit-CSP", "default-src 'self';");
      res.setHeader("X-Content-Security-Policy", "default-src 'self'");
      next();
    };
  },

  //rewrite connect middleware
  rewrite: function(){
    return function(req, res, next){
      var REWRITE = /\/(guide|api|cookbook|misc|tutorial|error).*$/,
          IGNORED = /(\.(css|js|png|jpg)$|partials\/.*\.html$)/,
          match;

      if (!IGNORED.test(req.url) && (match = req.url.match(REWRITE))) {
        console.log('rewriting', req.url);
        req.url = req.url.replace(match[0], '/index.html');
      }
      next();
    };
  },

  parallelTask: function(name) {
    var args = [name, '--port=' + this.lastParallelTaskPort];

    if (grunt.option('browsers')) {
      args.push('--browsers=' + grunt.option('browsers'));
    }

    if (grunt.option('reporters')) {
      args.push('--reporters=' + grunt.option('reporters'));
    }

    this.lastParallelTaskPort++;

    return {grunt: true, args: args};
  },

  lastParallelTaskPort: 9876
};
<commit_msg>fix(grunt): fix regex in grunt util to handle pre-release versions<commit_after>var fs = require('fs');
var shell = require('shelljs');
var grunt = require('grunt');
var spawn = require('child_process').spawn;
var version;

module.exports = {

  init: function() {
    if (!process.env.TRAVIS) {
      shell.exec('npm install');
    }
  },

  getVersion: function(){
    if (version) return version;

    var package = JSON.parse(fs.readFileSync('package.json', 'UTF-8'));
    // TODO(brian): change `(-|rc)` to `-` in the regex below after bower
    // fixes this issue: https://github.com/bower/bower/issues/782
    var match = package.version.match(/^([^\-]*)(?:(-|rc)(.+))?$/);
    var semver = match[1].split('.');
    var hash = shell.exec('git rev-parse --short HEAD', {silent: true}).output.replace('\n', '');

    var fullVersion = (match[1] + (match[2] ? '-' + hash : ''));
    version = {
      full: fullVersion,
      major: semver[0],
      minor: semver[1],
      dot: semver[2].replace(/rc\d+/, ''),
      codename: package.codename,
      cdn: package.cdnVersion
    };

    return version;
  },

  startKarma: function(config, singleRun, done){
    var browsers = grunt.option('browsers');
    var reporters = grunt.option('reporters');
    var noColor = grunt.option('no-colors');
    var port = grunt.option('port');
    var p = spawn('node', ['node_modules/karma/bin/karma', 'start', config,
      singleRun ? '--single-run=true' : '',
      reporters ? '--reporters=' + reporters : '',
      browsers ? '--browsers=' + browsers : '',
      noColor ? '--no-colors' : '',
      port ? '--port=' + port : ''
    ]);
    p.stdout.pipe(process.stdout);
    p.stderr.pipe(process.stderr);
    p.on('exit', function(code){
      if(code !== 0) grunt.fail.warn("Karma test(s) failed. Exit code: " + code);
      done();
    });
  },

  wrap: function(src, name){
    src.unshift('src/' + name + '.prefix');
    src.push('src/' + name + '.suffix');
    return src;
  },

  addStyle: function(src, styles, minify){
    styles = styles.map(processCSS.bind(this)).join('\n');
    src += styles;
    return src;

    function processCSS(file){
      var css = fs.readFileSync(file).toString();
      if(minify){
        css = css
          .replace(/\r?\n/g, '')
          .replace(/\/\*.*?\*\//g, '')
          .replace(/:\s+/g, ':')
          .replace(/\s*\{\s*/g, '{')
          .replace(/\s*\}\s*/g, '}')
          .replace(/\s*\,\s*/g, ',')
          .replace(/\s*\;\s*/g, ';');
      }
      //escape for js
      css = css
        .replace(/\\/g, '\\\\')
        .replace(/'/g, "\\'")
        .replace(/\r?\n/g, '\\n');
      return "angular.element(document).find('head').prepend('<style type=\"text/css\">" + css + "</style>');";
    }
  },

  process: function(src, NG_VERSION, strict){
    var processed = src
      .replace(/"NG_VERSION_FULL"/g, NG_VERSION.full)
      .replace(/"NG_VERSION_MAJOR"/, NG_VERSION.major)
      .replace(/"NG_VERSION_MINOR"/, NG_VERSION.minor)
      .replace(/"NG_VERSION_DOT"/, NG_VERSION.dot)
      .replace(/"NG_VERSION_CDN"/, NG_VERSION.cdn)
      .replace(/"NG_VERSION_CODENAME"/, NG_VERSION.codename);
    if (strict !== false) processed = this.singleStrict(processed, '\n\n', true);
    return processed;
  },

  build: function(config, fn){
    var files = grunt.file.expand(config.src);
    var styles = config.styles;
    //concat
    var src = files.map(function(filepath){
      return grunt.file.read(filepath);
    }).join(grunt.util.normalizelf('\n'));
    //process
    var processed = this.process(src, grunt.config('NG_VERSION'), config.strict);
    if (styles) processed = this.addStyle(processed, styles.css, styles.minify);
    //write
    grunt.file.write(config.dest, processed);
    grunt.log.ok('File ' + config.dest + ' created.');
    fn();
  },

  singleStrict: function(src, insert){
    return src
      .replace(/\s*("|')use strict("|');\s*/g, insert) // remove all file-specific strict mode flags
      .replace(/(\(function\([^)]*\)\s*\{)/, "$1'use strict';"); // add single strict mode flag
  },

  sourceMap: function(mapFile, fileContents) {
    // use the following once Chrome beta or stable supports the //# pragma
    // var sourceMapLine = '//# sourceMappingURL=' + mapFile + '\n';
    var sourceMapLine = '/*\n//@ sourceMappingURL=' + mapFile + '\n*/\n';
    return fileContents + sourceMapLine;
  },

  min: function(file, done) {
    var classPathSep = (process.platform === "win32") ? ';' : ':';
    var minFile = file.replace(/\.js$/, '.min.js');
    var mapFile = minFile + '.map';
    var mapFileName = mapFile.match(/[^\/]+$/)[0];
    var errorFileName = file.replace(/\.js$/, '-errors.json');
    var versionNumber = this.getVersion().number;
    shell.exec(
        'java ' +
            this.java32flags() + ' ' +
            '-Xmx2g ' +
            '-cp bower_components/closure-compiler/compiler.jar' + classPathSep +
            'bower_components/ng-closure-runner/ngcompiler.jar ' +
            'org.angularjs.closurerunner.NgClosureRunner ' +
            '--compilation_level SIMPLE_OPTIMIZATIONS ' +
            '--language_in ECMASCRIPT5_STRICT ' +
            '--minerr_pass ' +
            '--minerr_errors ' + errorFileName + ' ' +
            '--minerr_url http://errors.angularjs.org/' + versionNumber + '/ ' +
            '--source_map_format=V3 ' +
            '--create_source_map ' + mapFile + ' ' +
            '--js ' + file + ' ' +
            '--js_output_file ' + minFile,
      function(code) {
        if (code !== 0) grunt.fail.warn('Error minifying ' + file);

        // closure creates the source map relative to build/ folder, we need to strip those references
        grunt.file.write(mapFile, grunt.file.read(mapFile).replace('"file":"build/', '"file":"').
                                                           replace('"sources":["build/','"sources":["'));

        // move add use strict into the closure + add source map pragma
        grunt.file.write(minFile, this.sourceMap(mapFileName, this.singleStrict(grunt.file.read(minFile), '\n')));
        grunt.log.ok(file + ' minified into ' + minFile);
        done();
    }.bind(this));
  },

  //returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
  java32flags: function(){
    if (process.platform === "win32") return '';
    if (shell.exec('java -version -d32 2>&1', {silent: true}).code !== 0) return '';
    return ' -d32 -client';
  },

  //collects and combines error messages stripped out in minify step
  collectErrors: function () {
    var combined = {
      id: 'ng',
      generated: new Date().toString(),
      errors: {}
    };
    grunt.file.expand('build/*-errors.json').forEach(function (file) {
      var errors = grunt.file.readJSON(file),
        namespace;
      Object.keys(errors).forEach(function (prop) {
        if (typeof errors[prop] === 'object') {
          namespace = errors[prop];
          if (combined.errors[prop]) {
            Object.keys(namespace).forEach(function (code) {
              if (combined.errors[prop][code] && combined.errors[prop][code] !== namespace[code]) {
                grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
              } else {
                combined.errors[prop][code] = namespace[code];
              }
            });
          } else {
            combined.errors[prop] = namespace;
          }
        } else {
          if (combined.errors[prop] && combined.errors[prop] !== errors[prop]) {
            grunt.warn('[collect-errors] Duplicate minErr codes don\'t match!');
          } else {
            combined.errors[prop] = errors[prop];
          }
        }
      });
    });
    grunt.file.write('build/errors.json', JSON.stringify(combined));
    grunt.file.expand('build/*-errors.json').forEach(grunt.file.delete);
  },

  //csp connect middleware
  csp: function(){
    return function(req, res, next){
      res.setHeader("X-WebKit-CSP", "default-src 'self';");
      res.setHeader("X-Content-Security-Policy", "default-src 'self'");
      next();
    };
  },

  //rewrite connect middleware
  rewrite: function(){
    return function(req, res, next){
      var REWRITE = /\/(guide|api|cookbook|misc|tutorial|error).*$/,
          IGNORED = /(\.(css|js|png|jpg)$|partials\/.*\.html$)/,
          match;

      if (!IGNORED.test(req.url) && (match = req.url.match(REWRITE))) {
        console.log('rewriting', req.url);
        req.url = req.url.replace(match[0], '/index.html');
      }
      next();
    };
  },

  parallelTask: function(name) {
    var args = [name, '--port=' + this.lastParallelTaskPort];

    if (grunt.option('browsers')) {
      args.push('--browsers=' + grunt.option('browsers'));
    }

    if (grunt.option('reporters')) {
      args.push('--reporters=' + grunt.option('reporters'));
    }

    this.lastParallelTaskPort++;

    return {grunt: true, args: args};
  },

  lastParallelTaskPort: 9876
};
<|endoftext|>


<commit_before>"use strict";

const fs = require("fs-extra-promise");
const http = require("http");
const co = require("bluebird").coroutine;
const socketio = require("socket.io");
const koa = require("koa.io");
const bodyParser = require("koa-bodyparser");
const route = require("koa-route");
const staticFile = require("koa-static");
const HttpError = require("./http-error");
const CleanCSS = require("clean-css");

let Server = function(options) {
    let css;
    let cssTime;
    let app = koa();

    // Setup application
    app.name = options.name;
    app.use(bodyParser());

    // Configure error handling
    app.use(function*(next) {
        try {
            yield next;
        } catch (error) {
            console.error(error);
            console.error(error.stack);
            this.response.status = error.status || 500;
            this.type = "text/plain";
            this.body = error.message || error;
        }
    });

    // Serve static assets
    if (options.client) {
        app.use(route.get("/style.css", function*() {
            let stat = yield fs.stat(options.client + "/style.css");

            if (stat.mtime.toString() !== cssTime) {
                cssTime = stat.mtime.toString();

                let source1 = yield fs.readFile(__dirname + "/../client/style.css");
                let css1 = new CleanCSS({ root: __dirname + "/../client" }).minify(source1).styles;

                let source2 = yield fs.readFile(options.client + "/style.css");
                let css2 = new CleanCSS({ root: options.client }).minify(source2).styles;

                css = css1 + " " + css2;
            }

            this.type = "text/css";
            this.body = css;
        }));

        app.use(route.get("/mfw.js", function*() {
            let source = (yield fs.readFile(__dirname + "/../client/mfw.js")).toString();
            let libs = require(options.client + "/libs.json");
            let components = [];
            let items = yield fs.readdir(options.client + "/components");
            for (let item of items) {
                let stat = yield fs.stat(options.client + "/components/" + item);
                if (stat.isDirectory()) {
                    components.push(item);
                }
            }

            source = source.replace(/\/\*\* PATHS \*\*\//g, Object.keys(libs).map(name => name + ": \"" + libs[name] + "\"").join(",\n        "));
            //source = source.replace(/\/\*\* SHIM \*\*\//g, components.join(", ")); // TODO
            source = source.replace(/\/\*\* COMPONENTS \*\*\//g, components.map(name => "\"" + name + "\"").join(", "));

            this.type = "application/javascript";
            this.body = source;
        }));

        app.use(staticFile(options.client));
        app.use(staticFile(__dirname + "/../client"));
    }

    // Create routes if we have any
    if (options.routes) {
        for (let name of Object.keys(options.routes)) {
            let method = "get";
            let routeName = name;

            if (name[0] !== "/") {
                let parts = name.match(/(.+?)(\/.*)/);
                method = parts[1];
                routeName = parts[2];
            }

            app.use(route[method](routeName, options.routes[name]));
        }
    }

    // This must come after last app.use()
    let server = http.Server(app.callback());

    // Socket.io if we have defined API
    if (options.api) {
        let io = socketio(server);

        io.on("connection", function(socket) {
            for (let name of Object.keys(options.api)) {
                socket.on(name, function(data, ack) {
                    co(options.api[name])(data)
                    .then(function(result) {
                        ack(null, result);
                    })
                    .catch(function(error) {
                        ack(error.stack);
                    });
                });
            }
        });

        options.api.emit = function(event, data) {
            io.emit(event, data);
        };
    }

    this.start = function() {
        server.listen(options.port, options.bindTo);
        return this;
    };
};

module.exports = function(options) {
    return new Server(options);
};
<commit_msg>Bugfix<commit_after>"use strict";

const fs = require("fs-extra-promise");
const http = require("http");
const co = require("bluebird").coroutine;
const socketio = require("socket.io");
const koa = require("koa.io");
const bodyParser = require("koa-bodyparser");
const route = require("koa-route");
const staticFile = require("koa-static");
const HttpError = require("./http-error");
const CleanCSS = require("clean-css");

let Server = function(options) {
    let css;
    let cssTime;
    let app = koa();

    // Setup application
    app.name = options.name;
    app.use(bodyParser());

    // Configure error handling
    app.use(function*(next) {
        try {
            yield next;
        } catch (error) {
            console.error(error);
            console.error(error.stack);
            this.response.status = error.status || 500;
            this.type = "text/plain";
            this.body = error.message || error;
        }
    });

    // Serve static assets
    if (options.client) {
        app.use(route.get("/style.css", function*() {
            let stat = yield fs.statAsync(options.client + "/style.css");

            if (stat.mtime.toString() !== cssTime) {
                cssTime = stat.mtime.toString();

                let source1 = yield fs.readFileAsync(__dirname + "/../client/style.css");
                let css1 = new CleanCSS({ root: __dirname + "/../client" }).minify(source1).styles;

                let source2 = yield fs.readFileAsync(options.client + "/style.css");
                let css2 = new CleanCSS({ root: options.client }).minify(source2).styles;

                css = css1 + " " + css2;
            }

            this.type = "text/css";
            this.body = css;
        }));

        app.use(route.get("/mfw.js", function*() {
            let source = (yield fs.readFileAsync(__dirname + "/../client/mfw.js")).toString();
            let libs = require(options.client + "/libs.json");
            let components = [];
            let items = yield fs.readdirAsync(options.client + "/components");
            for (let item of items) {
                let stat = yield fs.statAsync(options.client + "/components/" + item);
                if (stat.isDirectory()) {
                    components.push(item);
                }
            }

            source = source.replace(/\/\*\* PATHS \*\*\//g, Object.keys(libs).map(name => name + ": \"" + libs[name] + "\"").join(",\n        "));
            //source = source.replace(/\/\*\* SHIM \*\*\//g, components.join(", ")); // TODO
            source = source.replace(/\/\*\* COMPONENTS \*\*\//g, components.map(name => "\"" + name + "\"").join(", "));

            this.type = "application/javascript";
            this.body = source;
        }));

        app.use(staticFile(options.client));
        app.use(staticFile(__dirname + "/../client"));
    }

    // Create routes if we have any
    if (options.routes) {
        for (let name of Object.keys(options.routes)) {
            let method = "get";
            let routeName = name;

            if (name[0] !== "/") {
                let parts = name.match(/(.+?)(\/.*)/);
                method = parts[1];
                routeName = parts[2];
            }

            app.use(route[method](routeName, options.routes[name]));
        }
    }

    // This must come after last app.use()
    let server = http.Server(app.callback());

    // Socket.io if we have defined API
    if (options.api) {
        let io = socketio(server);

        io.on("connection", function(socket) {
            for (let name of Object.keys(options.api)) {
                socket.on(name, function(data, ack) {
                    co(options.api[name])(data)
                    .then(function(result) {
                        ack(null, result);
                    })
                    .catch(function(error) {
                        ack(error.stack);
                    });
                });
            }
        });

        options.api.emit = function(event, data) {
            io.emit(event, data);
        };
    }

    this.start = function() {
        server.listen(options.port, options.bindTo);
        return this;
    };
};

module.exports = function(options) {
    return new Server(options);
};
<|endoftext|>


<commit_before>"use strict";

/*global nodeca, _*/

// stdlib
var readFileSync        = require('fs').readFileSync;
var createHttpServer    = require('http').createServer;
var createSecureServer  = require('https').createServer;

// 3rd-party
var async   = require('nlib').Vendor.Async;
var treeGet = require('nlib').Support.tree.get;

// internal
var listener = require('./server/http');

////////////////////////////////////////////////////////////////////////////////

// RegExp used to replace 0.0.0.0 and 127.*.*.* in hostname with `localhost`
var LOCAL_IP_RE = /^(?:127|0)(?:\.\d{1,3}){3}(:\d+)?$/;

// helper that starts http server and waits for it to bind to specified host and
// port (see configuration listen section).
//
function startServer(options, next) {
  var address = options.address,
      port    = options.port,
      server  = createHttpServer();

  function handle_server_error(err) {
    var err_prefix = "Can't bind to <" + address + "> with port <" + port + ">: ";

    if ('EADDRINUSE' === err.code) {
      next(err_prefix + 'Address in use...');
      return;
    }

    if ('EADDRNOTAVAIL' === err.code) {
      // system has no such ip address
      next(err_prefix + 'Address is not available...');
      return;
    }

    if ('ENOENT' === err.code) {
      // failed resolve addressname to ip address
      next(err_prefix + "Failed to resolve IP address...");
      return;
    }

    // unexpected / unknown error
    next(err_prefix + err);
  }

  server.on('error', handle_server_error);

  // start server
  server.listen(port, address, function () {
    server.removeListener('error', handle_server_error);
    next(null, server);
  });
}

function startSecureServer(options, next) {
  var address = options.address,
      port    = options.port,
      server  = createSecureServer(options);

  function handle_server_error(err) {
    var err_prefix = "Can't bind to <" + address + "> with port <" + port + ">: ";

    if ('EADDRINUSE' === err.code) {
      next(err_prefix + 'Address in use...');
      return;
    }

    if ('EADDRNOTAVAIL' === err.code) {
      // system has no such ip address
      next(err_prefix + 'Address is not available...');
      return;
    }

    if ('ENOENT' === err.code) {
      // failed resolve addressname to ip address
      next(err_prefix + "Failed to resolve IP address...");
      return;
    }

    // unexpected / unknown error
    next(err_prefix + err);
  }

  server.on('error', handle_server_error);

  // start server
  server.listen(port, address, function () {
    server.removeListener('error', handle_server_error);
    next(null, server);
  });
}

// Parse binding config and returns a parsed map:
//
//  {
//    '127.0.0.1:3000': {
//      address: '127.0.0.1',
//      port:    3000,
//      ssl:     null,
//      domains: [ 'localhost', ... ]
//    },
//    // ...
//  }
//
function extractBindings(config) {
  var result = {};

  //
  // Prepare config
  //

  _.each(config, function (options, key) {
    var host;

    if ('_' === key) {
      // skip special case keyword
      return;
    }

    if (!result[options.listen]) {
      result[options.listen] = {
        address:  options.listen.split(':')[0] || '0.0.0.0',
        port:     options.listen.split(':')[1] || 80,
        ssl:      null,
        hosts:    []
      };
    }

    result[options.listen].ssl = result[options.listen].ssl || options.ssl;

    host = (/^(?:https?:)?(?:\/\/([^\/]+)(?:\/|$))?/).exec(options.mount)[1];
    if (host && -1 === result[options.listen].hosts.indexOf(host)) {
      result[options.listen].hosts.push(host);
    }
  });

  //
  // Read `key`, `cert` and `pfx` files if given
  //

  _.each(result, function (options, key) {
    if (options.ssl) {
      _.each(options.ssl, function (file, part) {
        if ('pfx' === part || 'key' === part || 'cert' === part) {
          options.ssl[part] = readFileSync(file);
        }
      });
    }
  });

  return result;
}

// MODULE EXPORTS //////////////////////////////////////////////////////////////

// starts http server and attach application HTTP server and Faye realtime
// servers to it
//
module.exports = function (next) {
  var config       = nodeca.config.bind,
      bindings     = extractBindings(config),
      invalid_host = config._;

  try {
    // add support for webkit devtools
    require('webkit-devtools-agent');
    nodeca.logger.info('webkit-devtools-agent enabled');
  } catch (err) {
    // do nothing
    nodeca.logger.warn('webkit-devtools-agent disabled');
  }

  //
  // set "host not found" handler if it was not set
  //

  if (!_.isFunction(invalid_host)) {
    invalid_host = function (req, res) {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('Invalid host ' + req.headers.host);
    };
  }

  //
  // bind listeners
  //

  async.forEachSeries(_.keys(bindings), function (point, nextPoint) {
    var options = bindings[point];

    (options.ssl ? startSecureServer : startServer)(options, function (err, server) {
      if (err) {
        nextPoint(err);
        return;
      }

      // makes common preparations before
      // actual request processing
      function prepare(req, res, callback) {
        if (LOCAL_IP_RE.exec(req.headers.host)) {
          req.headers.host = 'localhost' + (RegExp.$1 || '');
        }

        callback();
      }

      nodeca.logger.info('Listening on ' + options.address + ':' + options.port +
                         ' ' + (options.ssl ? 'SSL' : 'NON-SSL'));

      listener.attach(server, prepare, nextPoint);
    });
  }, next);
};
<commit_msg>Refactor server init<commit_after>"use strict";

/*global nodeca, _*/

// stdlib
var readFileSync        = require('fs').readFileSync;
var createHttpServer    = require('http').createServer;
var createSecureServer  = require('https').createServer;

// 3rd-party
var async     = require('nlib').Vendor.Async;
var treeGet   = require('nlib').Support.tree.get;
var parseURL  = require('nlib').Vendor.Pointer.parseURL;

// internal
var listener = require('./server/http');

////////////////////////////////////////////////////////////////////////////////

// helper that starts http server and waits for it to bind to specified host and
// port (see configuration listen section).
//
function startServer(options, next) {
  var address = options.address,
      port    = options.port,
      server  = createHttpServer();

  function handle_server_error(err) {
    var err_prefix = "Can't bind to <" + address + "> with port <" + port + ">: ";

    if ('EADDRINUSE' === err.code) {
      next(err_prefix + 'Address in use...');
      return;
    }

    if ('EADDRNOTAVAIL' === err.code) {
      // system has no such ip address
      next(err_prefix + 'Address is not available...');
      return;
    }

    if ('ENOENT' === err.code) {
      // failed resolve addressname to ip address
      next(err_prefix + "Failed to resolve IP address...");
      return;
    }

    // unexpected / unknown error
    next(err_prefix + err);
  }

  server.on('error', handle_server_error);

  // start server
  server.listen(port, address, function () {
    server.removeListener('error', handle_server_error);
    next(null, server);
  });
}

function startSecureServer(options, next) {
  var address = options.address,
      port    = options.port,
      server  = createSecureServer(options);

  function handle_server_error(err) {
    var err_prefix = "Can't bind to <" + address + "> with port <" + port + ">: ";

    if ('EADDRINUSE' === err.code) {
      next(err_prefix + 'Address in use...');
      return;
    }

    if ('EADDRNOTAVAIL' === err.code) {
      // system has no such ip address
      next(err_prefix + 'Address is not available...');
      return;
    }

    if ('ENOENT' === err.code) {
      // failed resolve addressname to ip address
      next(err_prefix + "Failed to resolve IP address...");
      return;
    }

    // unexpected / unknown error
    next(err_prefix + err);
  }

  server.on('error', handle_server_error);

  // start server
  server.listen(port, address, function () {
    server.removeListener('error', handle_server_error);
    next(null, server);
  });
}

// Parse binding config and returns a parsed map:
//
//  {
//    '127.0.0.1:3000': {
//      address: '127.0.0.1',
//      port:    3000,
//      ssl:     null,
//      domains: [ 'localhost', ... ]
//    },
//    // ...
//  }
//
function extractBindings(config) {
  var result = {};

  //
  // Prepare config
  //

  _.each(config, function (options, key) {
    var host;

    if ('_' === key) {
      // skip special case keyword
      return;
    }

    if (!result[options.listen]) {
      result[options.listen] = {
        address:  options.listen.split(':')[0] || '0.0.0.0',
        port:     options.listen.split(':')[1] || 80,
        ssl:      null,
        hosts:    []
      };
    }

    result[options.listen].ssl = result[options.listen].ssl || options.ssl;

    host = parseURL(options.mount).host || '*';
    if (-1 === result[options.listen].hosts.indexOf(host)) {
      result[options.listen].hosts.push(host);
    }
  });

  //
  // Read `key`, `cert` and `pfx` files if given
  //

  _.each(result, function (options, key) {
    if (options.ssl) {
      _.each(options.ssl, function (file, part) {
        if ('pfx' === part || 'key' === part || 'cert' === part) {
          options.ssl[part] = readFileSync(file);
        }
      });
    }
  });

  return result;
}

// MODULE EXPORTS //////////////////////////////////////////////////////////////

// starts http server and attach application HTTP server and Faye realtime
// servers to it
//
module.exports = function (next) {
  var config       = nodeca.config.bind,
      bindings     = extractBindings(config),
      invalid_host = config._;

  try {
    // add support for webkit devtools
    require('webkit-devtools-agent');
    nodeca.logger.info('webkit-devtools-agent enabled');
  } catch (err) {
    // do nothing
    nodeca.logger.warn('webkit-devtools-agent disabled');
  }

  //
  // set "host not found" handler if it was not set
  //

  if (!_.isFunction(invalid_host)) {
    invalid_host = function (req, res) {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('Invalid host ' + req.headers.host);
    };
  }

  //
  // bind listeners
  //

  async.forEachSeries(_.keys(bindings), function (point, nextPoint) {
    var options = bindings[point];

    (options.ssl ? startSecureServer : startServer)(options, function (err, server) {
      var prepare;

      if (err) {
        nextPoint(err);
        return;
      }

      // makes common preparations before
      // actual request processing
      if (_.include(options.hosts, '*')) {
        prepare = function (req, res, callback) {
          callback();
        };
      } else {
        prepare = function (req, res, callback) {
          if (!_.include(options.hosts, req.headers.host)) {
            invalid_host(req, res);
            return;
          }

          callback();
        };
      }

      nodeca.logger.info('Listening on ' + options.address + ':' + options.port +
                         ' ' + (options.ssl ? 'SSL' : 'NON-SSL'));

      listener.attach(server, prepare, nextPoint);
    });
  }, next);
};
<|endoftext|>