<commit_before>
'use strict';

/**
 * Module dependencies.
 */

const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const escape = require('escape-html');
const typeis = require('type-is').is;
const statuses = require('statuses');
const destroy = require('destroy');
const assert = require('assert');
const extname = require('path').extname;
const vary = require('vary');
const only = require('only');
const util = require('util');
const encodeUrl = require('encodeurl');
const Stream = require('stream');

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },

  /**
   * Return response header.
   *
   * @return {Object}
   * @api public
   */

  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {}; // Node < 7.7
  },

  /**
   * Return response header, alias as response.header
   *
   * @return {Object}
   * @api public
   */

  get headers() {
    return this.header;
  },

  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status() {
    return this.res.statusCode;
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status(code) {
    if (this.headerSent) return;

    assert(Number.isInteger(code), 'status code must be a number');
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
    this._explicitStatus = true;
    this.res.statusCode = code;
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
    if (this.body && statuses.empty[code]) this.body = null;
  },

  /**
   * Get response status message
   *
   * @return {String}
   * @api public
   */

  get message() {
    return this.res.statusMessage || statuses[this.status];
  },

  /**
   * Set response status message
   *
   * @param {String} msg
   * @api public
   */

  set message(msg) {
    this.res.statusMessage = msg;
  },

  /**
   * Get response body.
   *
   * @return {Mixed}
   * @api public
   */

  get body() {
    return this._body;
  },

  /**
   * Set response body.
   *
   * @param {String|Buffer|Object|Stream} val
   * @api public
   */

  set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string' == typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if ('function' == typeof val.pipe) {
      onFinish(this.res, destroy.bind(null, val));
      ensureErrorHandler(val, err => this.ctx.onerror(err));

      // overwriting
      if (null != original && original != val) this.remove('Content-Length');

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

  /**
   * Set Content-Length field to `n`.
   *
   * @param {Number} n
   * @api public
   */

  set length(n) {
    this.set('Content-Length', n);
  },

  /**
   * Return parsed response Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length() {
    if (this.has('Content-Length')) {
      return parseInt(this.get('Content-Length'), 10) || 0;
    }

    const { body } = this;
    if (!body || body instanceof Stream) return undefined;
    if ('string' === typeof body) return Buffer.byteLength(body);
    if (Buffer.isBuffer(body)) return body.length;
    return Buffer.byteLength(JSON.stringify(body));
  },

  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent() {
    return this.res.headersSent;
  },

  /**
   * Vary on `field`.
   *
   * @param {String} field
   * @api public
   */

  vary(field) {
    if (this.headerSent) return;

    vary(this.res, field);
  },

  /**
   * Perform a 302 redirect to `url`.
   *
   * The string "back" is special-cased
   * to provide Referrer support, when Referrer
   * is not present `alt` or "/" is used.
   *
   * Examples:
   *
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   *
   * @param {String} url
   * @param {String} [alt]
   * @api public
   */

  redirect(url, alt) {
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', encodeUrl(url));

    // status
    if (!statuses.redirect[this.status]) this.status = 302;

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url);
      this.type = 'text/html; charset=utf-8';
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }

    // text
    this.type = 'text/plain; charset=utf-8';
    this.body = `Redirecting to ${url}.`;
  },

  /**
   * Set Content-Disposition header to "attachment" with optional `filename`.
   *
   * @param {String} filename
   * @api public
   */

  attachment(filename, options) {
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename, options));
  },

  /**
   * Set Content-Type response header with `type` through `mime.lookup()`
   * when it does not contain a charset.
   *
   * Examples:
   *
   *     this.type = '.html';
   *     this.type = 'html';
   *     this.type = 'json';
   *     this.type = 'application/json';
   *     this.type = 'png';
   *
   * @param {String} type
   * @api public
   */

  set type(type) {
    type = getType(type);
    if (type) {
      this.set('Content-Type', type);
    } else {
      this.remove('Content-Type');
    }
  },

  /**
   * Set the Last-Modified date using a string or a Date.
   *
   *     this.response.lastModified = new Date();
   *     this.response.lastModified = '2013-09-13';
   *
   * @param {String|Date} type
   * @api public
   */

  set lastModified(val) {
    if ('string' == typeof val) val = new Date(val);
    this.set('Last-Modified', val.toUTCString());
  },

  /**
   * Get the Last-Modified date in Date form, if it exists.
   *
   * @return {Date}
   * @api public
   */

  get lastModified() {
    const date = this.get('last-modified');
    if (date) return new Date(date);
  },

  /**
   * Set the ETag of a response.
   * This will normalize the quotes if necessary.
   *
   *     this.response.etag = 'md5hashsum';
   *     this.response.etag = '"md5hashsum"';
   *     this.response.etag = 'W/"123456789"';
   *
   * @param {String} etag
   * @api public
   */

  set etag(val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
    this.set('ETag', val);
  },

  /**
   * Get the ETag of a response.
   *
   * @return {String}
   * @api public
   */

  get etag() {
    return this.get('ETag');
  },

  /**
   * Return the response mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';', 1)[0];
  },

  /**
   * Check whether the response is one of the listed types.
   * Pretty much the same as `this.request.is()`.
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false}
   * @api public
   */

  is(type, ...types) {
    return typeis(this.type, type, ...types);
  },

  /**
   * Return response header.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   * @param {String} field
   * @return {String}
   * @api public
   */

  get(field) {
    return this.header[field.toLowerCase()] || '';
  },

  /**
   * Returns true if the header identified by name is currently set in the outgoing headers.
   * The header name matching is case-insensitive.
   *
   * Examples:
   *
   *     this.has('Content-Type');
   *     // => true
   *
   *     this.get('content-type');
   *     // => true
   *
   * @param {String} field
   * @return {boolean}
   * @api public
   */
  has(field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers;
  },

  /**
   * Set header `field` to `val`, or pass
   * an object of header fields.
   *
   * Examples:
   *
   *    this.set('Foo', ['bar', 'baz']);
   *    this.set('Accept', 'application/json');
   *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
   *
   * @param {String|Object|Array} field
   * @param {String} val
   * @api public
   */

  set(field, val) {
    if (this.headerSent) return;

    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
      else if (typeof val !== 'string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]);
      }
    }
  },

  /**
   * Append additional header `field` with value `val`.
   *
   * Examples:
   *
   * ```
   * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
   * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
   * this.append('Warning', '199 Miscellaneous warning');
   * ```
   *
   * @param {String} field
   * @param {String|Array} val
   * @api public
   */

  append(field, val) {
    const prev = this.get(field);

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val);
    }

    return this.set(field, val);
  },

  /**
   * Remove header `field`.
   *
   * @param {String} name
   * @api public
   */

  remove(field) {
    if (this.headerSent) return;

    this.res.removeHeader(field);
  },

  /**
   * Checks if the request is writable.
   * Tests for the existence of the socket
   * as node sometimes does not set it.
   *
   * @return {Boolean}
   * @api private
   */

  get writable() {
    // can't write any more after response finished
    if (this.res.finished) return false;

    const socket = this.res.socket;
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true;
    return socket.writable;
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (!this.res) return;
    const o = this.toJSON();
    o.body = this.body;
    return o;
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, [
      'status',
      'message',
      'header'
    ]);
  },

  /**
   * Flush any set headers, and begin the body
   */
  flushHeaders() {
    this.res.flushHeaders();
  }
};

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect;
}
<commit_msg>chore: support `writableEnded` (#1402)<commit_after>
'use strict';

/**
 * Module dependencies.
 */

const contentDisposition = require('content-disposition');
const ensureErrorHandler = require('error-inject');
const getType = require('cache-content-type');
const onFinish = require('on-finished');
const escape = require('escape-html');
const typeis = require('type-is').is;
const statuses = require('statuses');
const destroy = require('destroy');
const assert = require('assert');
const extname = require('path').extname;
const vary = require('vary');
const only = require('only');
const util = require('util');
const encodeUrl = require('encodeurl');
const Stream = require('stream');

/**
 * Prototype.
 */

module.exports = {

  /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },

  /**
   * Return response header.
   *
   * @return {Object}
   * @api public
   */

  get header() {
    const { res } = this;
    return typeof res.getHeaders === 'function'
      ? res.getHeaders()
      : res._headers || {}; // Node < 7.7
  },

  /**
   * Return response header, alias as response.header
   *
   * @return {Object}
   * @api public
   */

  get headers() {
    return this.header;
  },

  /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */

  get status() {
    return this.res.statusCode;
  },

  /**
   * Set response status code.
   *
   * @param {Number} code
   * @api public
   */

  set status(code) {
    if (this.headerSent) return;

    assert(Number.isInteger(code), 'status code must be a number');
    assert(code >= 100 && code <= 999, `invalid status code: ${code}`);
    this._explicitStatus = true;
    this.res.statusCode = code;
    if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code];
    if (this.body && statuses.empty[code]) this.body = null;
  },

  /**
   * Get response status message
   *
   * @return {String}
   * @api public
   */

  get message() {
    return this.res.statusMessage || statuses[this.status];
  },

  /**
   * Set response status message
   *
   * @param {String} msg
   * @api public
   */

  set message(msg) {
    this.res.statusMessage = msg;
  },

  /**
   * Get response body.
   *
   * @return {Mixed}
   * @api public
   */

  get body() {
    return this._body;
  },

  /**
   * Set response body.
   *
   * @param {String|Buffer|Object|Stream} val
   * @api public
   */

  set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string' == typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if ('function' == typeof val.pipe) {
      onFinish(this.res, destroy.bind(null, val));
      ensureErrorHandler(val, err => this.ctx.onerror(err));

      // overwriting
      if (null != original && original != val) this.remove('Content-Length');

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json';
  },

  /**
   * Set Content-Length field to `n`.
   *
   * @param {Number} n
   * @api public
   */

  set length(n) {
    this.set('Content-Length', n);
  },

  /**
   * Return parsed response Content-Length when present.
   *
   * @return {Number}
   * @api public
   */

  get length() {
    if (this.has('Content-Length')) {
      return parseInt(this.get('Content-Length'), 10) || 0;
    }

    const { body } = this;
    if (!body || body instanceof Stream) return undefined;
    if ('string' === typeof body) return Buffer.byteLength(body);
    if (Buffer.isBuffer(body)) return body.length;
    return Buffer.byteLength(JSON.stringify(body));
  },

  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent() {
    return this.res.headersSent;
  },

  /**
   * Vary on `field`.
   *
   * @param {String} field
   * @api public
   */

  vary(field) {
    if (this.headerSent) return;

    vary(this.res, field);
  },

  /**
   * Perform a 302 redirect to `url`.
   *
   * The string "back" is special-cased
   * to provide Referrer support, when Referrer
   * is not present `alt` or "/" is used.
   *
   * Examples:
   *
   *    this.redirect('back');
   *    this.redirect('back', '/index.html');
   *    this.redirect('/login');
   *    this.redirect('http://google.com');
   *
   * @param {String} url
   * @param {String} [alt]
   * @api public
   */

  redirect(url, alt) {
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', encodeUrl(url));

    // status
    if (!statuses.redirect[this.status]) this.status = 302;

    // html
    if (this.ctx.accepts('html')) {
      url = escape(url);
      this.type = 'text/html; charset=utf-8';
      this.body = `Redirecting to <a href="${url}">${url}</a>.`;
      return;
    }

    // text
    this.type = 'text/plain; charset=utf-8';
    this.body = `Redirecting to ${url}.`;
  },

  /**
   * Set Content-Disposition header to "attachment" with optional `filename`.
   *
   * @param {String} filename
   * @api public
   */

  attachment(filename, options) {
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename, options));
  },

  /**
   * Set Content-Type response header with `type` through `mime.lookup()`
   * when it does not contain a charset.
   *
   * Examples:
   *
   *     this.type = '.html';
   *     this.type = 'html';
   *     this.type = 'json';
   *     this.type = 'application/json';
   *     this.type = 'png';
   *
   * @param {String} type
   * @api public
   */

  set type(type) {
    type = getType(type);
    if (type) {
      this.set('Content-Type', type);
    } else {
      this.remove('Content-Type');
    }
  },

  /**
   * Set the Last-Modified date using a string or a Date.
   *
   *     this.response.lastModified = new Date();
   *     this.response.lastModified = '2013-09-13';
   *
   * @param {String|Date} type
   * @api public
   */

  set lastModified(val) {
    if ('string' == typeof val) val = new Date(val);
    this.set('Last-Modified', val.toUTCString());
  },

  /**
   * Get the Last-Modified date in Date form, if it exists.
   *
   * @return {Date}
   * @api public
   */

  get lastModified() {
    const date = this.get('last-modified');
    if (date) return new Date(date);
  },

  /**
   * Set the ETag of a response.
   * This will normalize the quotes if necessary.
   *
   *     this.response.etag = 'md5hashsum';
   *     this.response.etag = '"md5hashsum"';
   *     this.response.etag = 'W/"123456789"';
   *
   * @param {String} etag
   * @api public
   */

  set etag(val) {
    if (!/^(W\/)?"/.test(val)) val = `"${val}"`;
    this.set('ETag', val);
  },

  /**
   * Get the ETag of a response.
   *
   * @return {String}
   * @api public
   */

  get etag() {
    return this.get('ETag');
  },

  /**
   * Return the response mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

  get type() {
    const type = this.get('Content-Type');
    if (!type) return '';
    return type.split(';', 1)[0];
  },

  /**
   * Check whether the response is one of the listed types.
   * Pretty much the same as `this.request.is()`.
   *
   * @param {String|String[]} [type]
   * @param {String[]} [types]
   * @return {String|false}
   * @api public
   */

  is(type, ...types) {
    return typeis(this.type, type, ...types);
  },

  /**
   * Return response header.
   *
   * Examples:
   *
   *     this.get('Content-Type');
   *     // => "text/plain"
   *
   *     this.get('content-type');
   *     // => "text/plain"
   *
   * @param {String} field
   * @return {String}
   * @api public
   */

  get(field) {
    return this.header[field.toLowerCase()] || '';
  },

  /**
   * Returns true if the header identified by name is currently set in the outgoing headers.
   * The header name matching is case-insensitive.
   *
   * Examples:
   *
   *     this.has('Content-Type');
   *     // => true
   *
   *     this.get('content-type');
   *     // => true
   *
   * @param {String} field
   * @return {boolean}
   * @api public
   */
  has(field) {
    return typeof this.res.hasHeader === 'function'
      ? this.res.hasHeader(field)
      // Node < 7.7
      : field.toLowerCase() in this.headers;
  },

  /**
   * Set header `field` to `val`, or pass
   * an object of header fields.
   *
   * Examples:
   *
   *    this.set('Foo', ['bar', 'baz']);
   *    this.set('Accept', 'application/json');
   *    this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
   *
   * @param {String|Object|Array} field
   * @param {String} val
   * @api public
   */

  set(field, val) {
    if (this.headerSent) return;

    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
      else if (typeof val !== 'string') val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (const key in field) {
        this.set(key, field[key]);
      }
    }
  },

  /**
   * Append additional header `field` with value `val`.
   *
   * Examples:
   *
   * ```
   * this.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
   * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
   * this.append('Warning', '199 Miscellaneous warning');
   * ```
   *
   * @param {String} field
   * @param {String|Array} val
   * @api public
   */

  append(field, val) {
    const prev = this.get(field);

    if (prev) {
      val = Array.isArray(prev)
        ? prev.concat(val)
        : [prev].concat(val);
    }

    return this.set(field, val);
  },

  /**
   * Remove header `field`.
   *
   * @param {String} name
   * @api public
   */

  remove(field) {
    if (this.headerSent) return;

    this.res.removeHeader(field);
  },

  /**
   * Checks if the request is writable.
   * Tests for the existence of the socket
   * as node sometimes does not set it.
   *
   * @return {Boolean}
   * @api private
   */

  get writable() {
    // can't write any more after response finished
    // response.writableEnded is available since Node > 12.9
    // https://nodejs.org/api/http.html#http_response_writableended
    // response.finished is undocumented feature of previous Node versions
    // https://stackoverflow.com/questions/16254385/undocumented-response-finished-in-node-js
    if (this.res.writableEnded || this.res.finished) return false;

    const socket = this.res.socket;
    // There are already pending outgoing res, but still writable
    // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
    if (!socket) return true;
    return socket.writable;
  },

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (!this.res) return;
    const o = this.toJSON();
    o.body = this.body;
    return o;
  },

  /**
   * Return JSON representation.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, [
      'status',
      'message',
      'header'
    ]);
  },

  /**
   * Flush any set headers, and begin the body
   */
  flushHeaders() {
    this.res.flushHeaders();
  }
};

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect;
}
<|endoftext|>


<commit_before>import Promise from './promise';
import {
  isFunction
} from './utils';

/**
 `RSVP.map` is similar to JavaScript's native `map` method, except that it
  waits for all promises to become fulfilled before running the `mapFn` on
  each item in given to `promises`. `RSVP.map` returns a promise that will
  become fulfilled with the result of running `mapFn` on the values the promises
  become fulfilled with.

  For example:

  ```javascript

  let promise1 = RSVP.resolve(1);
  let promise2 = RSVP.resolve(2);
  let promise3 = RSVP.resolve(3);
  let promises = [ promise1, promise2, promise3 ];

  let mapFn = function(item){
    return item + 1;
  };

  RSVP.map(promises, mapFn).then(function(result){
    // result is [ 2, 3, 4 ]
  });
  ```

  If any of the `promises` given to `RSVP.map` are rejected, the first promise
  that is rejected will be given as an argument to the returned promise's
  rejection handler. For example:

  ```javascript
  let promise1 = RSVP.resolve(1);
  let promise2 = RSVP.reject(new Error('2'));
  let promise3 = RSVP.reject(new Error('3'));
  let promises = [ promise1, promise2, promise3 ];

  let mapFn = function(item){
    return item + 1;
  };

  RSVP.map(promises, mapFn).then(function(array){
    // Code here never runs because there are rejected promises!
  }, function(reason) {
    // reason.message === '2'
  });
  ```

  `RSVP.map` will also wait if a promise is returned from `mapFn`. For example,
  say you want to get all comments from a set of blog posts, but you need
  the blog posts first because they contain a url to those comments.

  ```javscript

  let mapFn = function(blogPost){
    // getComments does some ajax and returns an RSVP.Promise that is fulfilled
    // with some comments data
    return getComments(blogPost.comments_url);
  };

  // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled
  // with some blog post data
  RSVP.map(getBlogPosts(), mapFn).then(function(comments){
    // comments is the result of asking the server for the comments
    // of all blog posts returned from getBlogPosts()
  });
  ```

  @method map
  @static
  @for RSVP
  @param {Array} promises
  @param {Function} mapFn function to be called on each fulfilled promise.
  @param {String} label optional string for labeling the promise.
  Useful for tooling.
  @return {Promise} promise that is fulfilled with the result of calling
  `mapFn` on each fulfilled promise or value when they become fulfilled.
   The promise will be rejected if any of the given `promises` become rejected.
  @static
*/
export default function map(promises, mapFn, label) {
  return Promise.all(promises, label).then(values => {
    if (!isFunction(mapFn)) {
      throw new TypeError("You must pass a function as map's second argument.");
    }

    let length = values.length;
    let results = new Array(length);

    for (let i = 0; i < length; i++) {
      results[i] = mapFn(values[i]);
    }

    return Promise.all(results, label);
  });
}
<commit_msg>fail faster when `mapFn` is not supplied for `map`<commit_after>import Promise from './promise';

import {
  isFunction
} from './utils';

/**
 `RSVP.map` is similar to JavaScript's native `map` method, except that it
  waits for all promises to become fulfilled before running the `mapFn` on
  each item in given to `promises`. `RSVP.map` returns a promise that will
  become fulfilled with the result of running `mapFn` on the values the promises
  become fulfilled with.

  For example:

  ```javascript

  let promise1 = RSVP.resolve(1);
  let promise2 = RSVP.resolve(2);
  let promise3 = RSVP.resolve(3);
  let promises = [ promise1, promise2, promise3 ];

  let mapFn = function(item){
    return item + 1;
  };

  RSVP.map(promises, mapFn).then(function(result){
    // result is [ 2, 3, 4 ]
  });
  ```

  If any of the `promises` given to `RSVP.map` are rejected, the first promise
  that is rejected will be given as an argument to the returned promise's
  rejection handler. For example:

  ```javascript
  let promise1 = RSVP.resolve(1);
  let promise2 = RSVP.reject(new Error('2'));
  let promise3 = RSVP.reject(new Error('3'));
  let promises = [ promise1, promise2, promise3 ];

  let mapFn = function(item){
    return item + 1;
  };

  RSVP.map(promises, mapFn).then(function(array){
    // Code here never runs because there are rejected promises!
  }, function(reason) {
    // reason.message === '2'
  });
  ```

  `RSVP.map` will also wait if a promise is returned from `mapFn`. For example,
  say you want to get all comments from a set of blog posts, but you need
  the blog posts first because they contain a url to those comments.

  ```javscript

  let mapFn = function(blogPost){
    // getComments does some ajax and returns an RSVP.Promise that is fulfilled
    // with some comments data
    return getComments(blogPost.comments_url);
  };

  // getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled
  // with some blog post data
  RSVP.map(getBlogPosts(), mapFn).then(function(comments){
    // comments is the result of asking the server for the comments
    // of all blog posts returned from getBlogPosts()
  });
  ```

  @method map
  @static
  @for RSVP
  @param {Array} promises
  @param {Function} mapFn function to be called on each fulfilled promise.
  @param {String} label optional string for labeling the promise.
  Useful for tooling.
  @return {Promise} promise that is fulfilled with the result of calling
  `mapFn` on each fulfilled promise or value when they become fulfilled.
   The promise will be rejected if any of the given `promises` become rejected.
  @static
*/
export default function map(promises, mapFn, label) {
  if (!isFunction(mapFn)) {
    return Promise.reject(new TypeError("You must pass a function as map's second argument."), label);
  }

  return Promise.all(promises, label).then(values => {
    let length = values.length;
    let results = new Array(length);

    for (let i = 0; i < length; i++) {
      results[i] = mapFn(values[i]);
    }

    return Promise.all(results, label);
  });
}
<|endoftext|>


<commit_before>"use strict";
var assert = require('assert');
var R = require('ramda');
var Q = require('q');
var Mailer = function Mailer() {};
var sendGridLib = require('sendgrid');
var helper = require('sendgrid').mail;
/* eslint-disable no-unused-vars */
var sendgridInstance = {};
var sendgridMailSettings = {};
var sendgridTrackingSettings = {};
/* eslint-enable no-unused-vars */

var SendgridConnector = function SendgridConnector(settings) {
  assert(typeof settings === 'object', 'cannot init connector without settings');
  assert(typeof settings.api_key === 'string', 'cannot init connector without api key');
  if (settings.api_key){
    this.sendgrid = sendGridLib(settings.api_key); //eslint-disable-line
  }

  this.sendgridMailSettings = settings.mail_settings;
  this.sendgridTrackingSettings = settings.tracking_settings;
  sendgridMailSettings = this.sendgridMailSettings;
  sendgridTrackingSettings = this.sendgridTrackingSettings;
  sendgridInstance = this.sendgrid;
};

SendgridConnector.initialize = function (dataSource, callback) {
  dataSource.connector = new SendgridConnector(dataSource.settings);
  callback();
};

SendgridConnector.prototype.DataAccessObject = Mailer;

/**
 * Send transactional email with options
 * Full list of options are available here: https://www.npmjs.com/package/sendgrid#available-params
 *
 * @param {Object} options
 * {
 *   from: { name: "John", email: "john@cellarise.com" },
 *   to: "mail@cellarise.com",
 *   subject: "Test mail",
 *   text: "Plain text message",
 *   html: "Html messages here"
 * }
 *
 * @param {Function} cb callback
 * @returns {Function} deferred promise
 */
Mailer.send = function (options, cb) { // eslint-disable-line
  var dataSource = this.dataSource,
    connector = dataSource.connector,
    deferred = Q.defer(),
    sendgridMessage = {},
    sendgridEmail,
    request;

  var fn = function (err, result) {
    if (err) {
      deferred.reject(err);
    } else {
      deferred.resolve(result);
    }
    return cb && cb(err, result);
  };

  if (options.__data) {
    options = R.clone(options.__data);
  } else {
    options = R.clone(options);
  }

  assert(connector, 'Cannot send mail without a connector!');

  if (connector.sendgrid) {

    //email from param
    if (R.is(String, options.from)) {
      sendgridMessage.from = options.from;
    } else if (R.is(Object, options.from)) {
      sendgridMessage.fromname = options.from.name;
      if (options.from.hasOwnProperty('address')) {
        sendgridMessage.from = options.from.address;
      } else if (options.from.hasOwnProperty('email')) {
        sendgridMessage.from = options.from.email;
      }
    } else {
      if (options.from_name) {
        sendgridMessage.fromname = options.from_name || '';
      }
      if (options.from_email) {
        sendgridMessage.from = options.from_email || '';
      }
    }
    delete options.from;

    //email to param
    if (R.is(String, options.to)) {
      options.to = R.map(
        function unwrapEmailString(email) {
          return {"email": email};
        },
        options.to.split([',', ';'])
      );
    }

    if (R.is(Object, options.to) && !R.is(Array, options.to)) {
      options.to = [options.to];
    }

    if (R.is(Array, options.to)) {
      sendgridMessage.to = R.map(
        function unwrapArrayString(recipient) {
          if (R.is(String, recipient)) {
            recipient = {"email": recipient};
          }
          return new helper.Email(recipient.email, recipient.name);
        },
        options.to
      );
    } else {
      throw new Error('invalid format for "to"');
    }

    delete options.to;

    //basic sendgrid email
    sendgridEmail = new helper.Mail(
      new helper.Email(sendgridMessage.from, sendgridMessage.fromname),
      options.subject,
      sendgridMessage.to[0], // needs major refactoring to send to each 'to'
      new helper.Content("text/plain", options.text || '')
    );
    if (options.html) {
      sendgridEmail.addContent(new helper.Content("text/html", options.html));
    }

    if (R.is(Array, sendgridEmail.personalizations) && sendgridEmail.personalizations.length > 0) {
      if (options.cc) {
        sendgridEmail.personalizations[0].addCc(options.cc);
      }
      if (options.bcc) {
        sendgridEmail.personalizations[0].addBcc(options.bcc);
      }
    }

    //attachments
    if (R.is(Array, options.attachments)) {
      R.forEach(function eachFile(fileConfig){
        sendgridEmail.addAttachment(fileConfig);
      }, options.attachments);
    }
    if (options.files) {
      R.forEach(function eachFile(fileConfig){
        sendgridEmail.addAttachment(fileConfig);
      }, options.files);
    }

    //sendgrid mail settings
    if (sendgridMailSettings) {
      sendgridEmail.addMailSettings(sendgridMailSettings);
    }
    //trackingSettings
    if (sendgridTrackingSettings) {
      sendgridEmail.addTrackingSettings(sendgridTrackingSettings);
    }
    //Sendgrid options
    if (options.sendGridConfig) {
      //personalizations
      if (R.is(Array, options.sendGridConfig.personalizations)) {
        R.forEach(function eachPers(personalization){
          sendgridEmail.addPersonalization(personalization);
        }, options.sendGridConfig.personalizations);
      }
      //sections
      if (R.is(Array, options.sendGridConfig.sections)) {
        R.forEach(function eachSec(section){
          sendgridEmail.addSection(section);
        }, options.sendGridConfig.sections);
      }
      //headers
      if (R.is(Array, options.sendGridConfig.headers)) {
        R.forEach(function eachHeader(header){
          sendgridEmail.addHeader(header);
        }, options.sendGridConfig.headers);
      }
      //templateId
      if (options.sendGridConfig.templateId) {
        sendgridEmail.setTemplateId(options.sendGridConfig.templateId);
      }
      //sendAt
      if (options.sendGridConfig.sendAt) {
        sendgridEmail.setSendAt(options.sendGridConfig.sendAt);
      }
      //batchId
      if (options.sendGridConfig.batchId) {
        sendgridEmail.setBatchId(options.sendGridConfig.batchId);
      }
      //asm
      if (options.sendGridConfig.asm) {
        sendgridEmail.setAsm(options.sendGridConfig.asm);
      }
      //ipPoolName
      if (options.sendGridConfig.ipPoolName) {
        sendgridEmail.setIpPoolName(options.sendGridConfig.ipPoolName);
      }
      //replyTo
      if (options.sendGridConfig.replyTo) {
        sendgridEmail.setReplyTo(options.sendGridConfig.replyTo);
      }
      //categories
      if (R.is(Array, options.sendGridConfig.categories)) {
        R.forEach(function eachCategory(category){
          sendgridEmail.addCategory(category);
        }, options.sendGridConfig.categories);
      }
    }
    request = connector.sendgrid.emptyRequest();
    request.method = 'POST';
    request.path = '/v3/mail/send';
    request.body = sendgridEmail.toJSON();
    connector.sendgrid.API(request, function sgCb(response){ // eslint-disable-line
      var code = response && response.statusCode;
      if (code && (code.toString() === "200" || code.toString() === "202")) {
        fn(null, response);
      } else {
        fn(response);
      }
    });
  } else {
    process.nextTick(function nextTick() { // eslint-disable-line
      fn(null, options);
    });
  }
  return deferred.promise;
};

/**
 * Send an email instance using instance
 */

Mailer.prototype.send = function protoSend(fn) {
  return this.constructor.send(this, fn);
};

/**
 * Export the connector class
 */
module.exports = SendgridConnector;
<commit_msg>Let custom personalizations override default personalization, this stops the to field from becoming it's own personalization<commit_after>"use strict";
var assert = require('assert');
var R = require('ramda');
var Q = require('q');
var Mailer = function Mailer() {};
var sendGridLib = require('sendgrid');
var helper = require('sendgrid').mail;
/* eslint-disable no-unused-vars */
var sendgridInstance = {};
var sendgridMailSettings = {};
var sendgridTrackingSettings = {};
/* eslint-enable no-unused-vars */

var SendgridConnector = function SendgridConnector(settings) {
  assert(typeof settings === 'object', 'cannot init connector without settings');
  assert(typeof settings.api_key === 'string', 'cannot init connector without api key');
  if (settings.api_key){
    this.sendgrid = sendGridLib(settings.api_key); //eslint-disable-line
  }

  this.sendgridMailSettings = settings.mail_settings;
  this.sendgridTrackingSettings = settings.tracking_settings;
  sendgridMailSettings = this.sendgridMailSettings;
  sendgridTrackingSettings = this.sendgridTrackingSettings;
  sendgridInstance = this.sendgrid;
};

SendgridConnector.initialize = function (dataSource, callback) {
  dataSource.connector = new SendgridConnector(dataSource.settings);
  callback();
};

SendgridConnector.prototype.DataAccessObject = Mailer;

/**
 * Send transactional email with options
 * Full list of options are available here: https://www.npmjs.com/package/sendgrid#available-params
 *
 * @param {Object} options
 * {
 *   from: { name: "John", email: "john@cellarise.com" },
 *   to: "mail@cellarise.com",
 *   subject: "Test mail",
 *   text: "Plain text message",
 *   html: "Html messages here"
 * }
 *
 * @param {Function} cb callback
 * @returns {Function} deferred promise
 */
Mailer.send = function (options, cb) { // eslint-disable-line
  var dataSource = this.dataSource,
    connector = dataSource.connector,
    deferred = Q.defer(),
    sendgridMessage = {},
    sendgridEmail,
    request;

  var fn = function (err, result) {
    if (err) {
      deferred.reject(err);
    } else {
      deferred.resolve(result);
    }
    return cb && cb(err, result);
  };

  if (options.__data) {
    options = R.clone(options.__data);
  } else {
    options = R.clone(options);
  }

  assert(connector, 'Cannot send mail without a connector!');

  if (connector.sendgrid) {

    //email from param
    if (R.is(String, options.from)) {
      sendgridMessage.from = options.from;
    } else if (R.is(Object, options.from)) {
      sendgridMessage.fromname = options.from.name;
      if (options.from.hasOwnProperty('address')) {
        sendgridMessage.from = options.from.address;
      } else if (options.from.hasOwnProperty('email')) {
        sendgridMessage.from = options.from.email;
      }
    } else {
      if (options.from_name) {
        sendgridMessage.fromname = options.from_name || '';
      }
      if (options.from_email) {
        sendgridMessage.from = options.from_email || '';
      }
    }
    delete options.from;

    //email to param
    if (R.is(String, options.to)) {
      options.to = R.map(
        function unwrapEmailString(email) {
          return {"email": email};
        },
        options.to.split([',', ';'])
      );
    }

    if (R.is(Object, options.to) && !R.is(Array, options.to)) {
      options.to = [options.to];
    }

    if (R.is(Array, options.to)) {
      sendgridMessage.to = R.map(
        function unwrapArrayString(recipient) {
          if (R.is(String, recipient)) {
            recipient = {"email": recipient};
          }
          return new helper.Email(recipient.email, recipient.name);
        },
        options.to
      );
    } else {
      throw new Error('invalid format for "to"');
    }

    delete options.to;

    //basic sendgrid email
    sendgridEmail = new helper.Mail(
      new helper.Email(sendgridMessage.from, sendgridMessage.fromname),
      options.subject,
      sendgridMessage.to[0], // needs major refactoring to send to each 'to'
      new helper.Content("text/plain", options.text || '')
    );
    if (options.html) {
      sendgridEmail.addContent(new helper.Content("text/html", options.html));
    }

    if (R.is(Array, sendgridEmail.personalizations) && sendgridEmail.personalizations.length > 0) {
      if (options.cc) {
        sendgridEmail.personalizations[0].addCc(options.cc);
      }
      if (options.bcc) {
        sendgridEmail.personalizations[0].addBcc(options.bcc);
      }
    }

    //attachments
    if (R.is(Array, options.attachments)) {
      R.forEach(function eachFile(fileConfig){
        sendgridEmail.addAttachment(fileConfig);
      }, options.attachments);
    }
    if (options.files) {
      R.forEach(function eachFile(fileConfig){
        sendgridEmail.addAttachment(fileConfig);
      }, options.files);
    }

    //sendgrid mail settings
    if (sendgridMailSettings) {
      sendgridEmail.addMailSettings(sendgridMailSettings);
    }
    //trackingSettings
    if (sendgridTrackingSettings) {
      sendgridEmail.addTrackingSettings(sendgridTrackingSettings);
    }
    //Sendgrid options
    if (options.sendGridConfig) {
      //personalizations
      if (R.is(Array, options.sendGridConfig.personalizations)) {
        sendgridEmail.personalizations = options.sendGridConfig.personalizations;
      }
      //sections
      if (R.is(Array, options.sendGridConfig.sections)) {
        R.forEach(function eachSec(section){
          sendgridEmail.addSection(section);
        }, options.sendGridConfig.sections);
      }
      //headers
      if (R.is(Array, options.sendGridConfig.headers)) {
        R.forEach(function eachHeader(header){
          sendgridEmail.addHeader(header);
        }, options.sendGridConfig.headers);
      }
      //templateId
      if (options.sendGridConfig.templateId) {
        sendgridEmail.setTemplateId(options.sendGridConfig.templateId);
      }
      //sendAt
      if (options.sendGridConfig.sendAt) {
        sendgridEmail.setSendAt(options.sendGridConfig.sendAt);
      }
      //batchId
      if (options.sendGridConfig.batchId) {
        sendgridEmail.setBatchId(options.sendGridConfig.batchId);
      }
      //asm
      if (options.sendGridConfig.asm) {
        sendgridEmail.setAsm(options.sendGridConfig.asm);
      }
      //ipPoolName
      if (options.sendGridConfig.ipPoolName) {
        sendgridEmail.setIpPoolName(options.sendGridConfig.ipPoolName);
      }
      //replyTo
      if (options.sendGridConfig.replyTo) {
        sendgridEmail.setReplyTo(options.sendGridConfig.replyTo);
      }
      //categories
      if (R.is(Array, options.sendGridConfig.categories)) {
        R.forEach(function eachCategory(category){
          sendgridEmail.addCategory(category);
        }, options.sendGridConfig.categories);
      }
    }
    request = connector.sendgrid.emptyRequest();
    request.method = 'POST';
    request.path = '/v3/mail/send';
    request.body = sendgridEmail.toJSON();
    connector.sendgrid.API(request, function sgCb(response){ // eslint-disable-line
      var code = response && response.statusCode;
      if (code && (code.toString() === "200" || code.toString() === "202")) {
        fn(null, response);
      } else {
        fn(response);
      }
    });
  } else {
    process.nextTick(function nextTick() { // eslint-disable-line
      fn(null, options);
    });
  }
  return deferred.promise;
};

/**
 * Send an email instance using instance
 */

Mailer.prototype.send = function protoSend(fn) {
  return this.constructor.send(this, fn);
};

/**
 * Export the connector class
 */
module.exports = SendgridConnector;
<|endoftext|>


<commit_before>/**
 * The Services class initializes the Shell's API services.
 */

'use strict';

const _ = require('lodash'),
    express = require('express'),
    cors = require('cors'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    morgan = require('morgan'),
    helmet = require('helmet'),
    serverUtils = require('./server-utils'),
    {SocketIOLoader, Loader} = require('./api');

class Services {

    /**
     * @description Sets up HTTP and Socket APIs using routes, socket connections, and configuration functions defined by LabShare API packages
     * @param {Object} [options]
     * @param {Object} [options.security] - Options to pass to the helmet, cors, and express-session libraries
     * @param {Boolean} [options.loadServices] - Do not load APIs if false. Default: true
     * @param {String} [options.main] - A relative or absolute path to a directory containing a LabShare package. Default: process.cwd()
     * @param {String} [options.pattern] - The pattern used to match LabShare API modules
     * @param {Object} [options.logger] - Error logging provider. It must define an `error` function. Default: console
     * @param {Array} [options.directories] - A list of paths to LabShare packages that should be searched for API modules. Directories
     * that do not contain a package.json are ignored. Default: []
     * @param {Array} [options.ignore] - A list of LabShare package names that should be ignored by the API and Socket loaders. Default: []
     */
    constructor(options = {}) {
        let logger = _.get(global.LabShare, 'Logger', console);  // Default to the logger instance initialized by the LSC LabShare package

        this._app = express();
        this._initialized = false;
        this._servicesActive = false;
        this._isProduction = this._app.get('env') === 'production';

        this._options = _.defaultsDeep(options, {
            logger,
            connections: _.get(global.LabShare, 'Config.services.Socket.Connections'),
            loadServices: true,
            pattern: '{src/api,api}/*.js',
            main: process.cwd(),
            directories: [],
            morgan: {
                enable: true,
                format: this._isProduction ? 'combined' : 'dev',
                options: {
                    // Workaround to add fluentD integration with the morgan logging library
                    stream: _.get(this.logger, 'stream.write')
                }
            },
            security: _.defaultsDeep(_.get(global.LabShare, 'Config.services.security', {}), {
                sessionOptions: {
                    secret: require('crypto').randomBytes(64).toString('hex'),
                    resave: false,
                    saveUninitialized: false,
                    name: 'sessionID',
                    cookie: {
                        httpOnly: true,
                        maxAge: 60 * 60 * 1000,      // 1 hour
                        secure: this._isProduction   // only allow SSL cookies in production by default
                    }
                },
                contentSecurityPolicy: false,
                hpkp: false,
                referrerPolicy: {
                    policy: 'no-referrer'
                },
                corsOptions: {
                    origin: '*',
                    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
                    preflightContinue: false
                }
            })
        });

        this.server = serverUtils.createServer(this._app, this._options.logger);
        this._apiLoader = new Loader(this._app, this._options);
        this._socketLoader = new SocketIOLoader(this.server, this._options);
    }

    /**
     * @description Load the services and assign middleware but do not start up the server or establish
     * any socket connections yet
     * @api
     */
    initialize() {
        this._initialized = true;

        this._app.use(helmet(this._options.security));
        this._app.use(bodyParser.json());
        this._app.use(bodyParser.urlencoded({extended: true}));
        this._app.use(cors(this._options.security.corsOptions));

        if (this._options.morgan.enable) {
            this._app.use(morgan(this._options.morgan.format, this._options.morgan.options));
        }

        this._app.use(cookieParser());

        // See: https://github.com/expressjs/session#cookiesecure
        if (this._isProduction) {
            this._app.set('trust proxy', 1);  // trust first proxy
        }

        this._app.use(require('express-session')(this._options.security.sessionOptions));

        this._socketLoader.initialize();
        this._apiLoader.initialize();
    }

    /**
     * @description Allows additional modifications to be made to the Express instance and routes before the services are started up.
     * @param {Function} func - A configuration function that receives all the API routes and the express app as arguments
     * @api
     */
    config(func) {
        if (!this._initialized) {
            this.initialize();
        }

        if (this._servicesActive) {
            throw new Error('You cannot modify the LabShare API services after starting up the server!');
        }

        func({
            services: this._apiLoader.services,
            sockets: this._socketLoader.getSockets(),
            app: this._app,
            io: this.io()
        });
    }

    /**
     * @description Starts the server and sets up socket connections
     * @returns {Object} The Node.js HTTP or HTTPS server
     * @api
     */
    start() {
        if (!this._initialized) {
            this.initialize();
        }

        // Run all the package API 'config' functions
        this._apiLoader.setConfig({
            apiLoader: this._apiLoader,
            app: this._app
        });

        this._servicesActive = true;

        serverUtils.startServer(this.server, this._options.logger);

        // add API routes and socket connections unless configured not to
        if (this._options.loadServices) {
            this._apiLoader.setAPIs(_.get(global.LabShare, 'Config.services.ServicePath') || '/');

            this._socketLoader.connect();
            this._socketLoader.on('error', error => {
                this._options.logger.error(error);
            });
            this._socketLoader.on('status', message => {
                this._options.logger.info(message);
            });

            // TODO: perform this global assignment OUTSIDE of the Services class if possible

            _.set(global, 'LabShare.IO', this.io());
        } else {
            this._options.logger.warn('API services are disabled because Service\'s `options.loadServices` is missing or set to false!');
        }

        return this.server;
    }

    io() {
        return this._socketLoader.getIO();
    }
}

module.exports = Services;

// if executed as a standalone script...
if (!module.parent) {
    let services = new Services();
    services.start();
}
<commit_msg>Do not assign the global.LabShare.IO instance if it already exists<commit_after>/**
 * The Services class initializes the Shell's API services.
 */

'use strict';

const _ = require('lodash'),
    express = require('express'),
    cors = require('cors'),
    cookieParser = require('cookie-parser'),
    bodyParser = require('body-parser'),
    morgan = require('morgan'),
    helmet = require('helmet'),
    serverUtils = require('./server-utils'),
    {SocketIOLoader, Loader} = require('./api');

class Services {

    /**
     * @description Sets up HTTP and Socket APIs using routes, socket connections, and configuration functions defined by LabShare API packages
     * @param {Object} [options]
     * @param {Object} [options.security] - Options to pass to the helmet, cors, and express-session libraries
     * @param {Boolean} [options.loadServices] - Do not load APIs if false. Default: true
     * @param {String} [options.main] - A relative or absolute path to a directory containing a LabShare package. Default: process.cwd()
     * @param {String} [options.pattern] - The pattern used to match LabShare API modules
     * @param {Object} [options.logger] - Error logging provider. It must define an `error` function. Default: console
     * @param {Array} [options.directories] - A list of paths to LabShare packages that should be searched for API modules. Directories
     * that do not contain a package.json are ignored. Default: []
     * @param {Array} [options.ignore] - A list of LabShare package names that should be ignored by the API and Socket loaders. Default: []
     */
    constructor(options = {}) {
        let logger = _.get(global.LabShare, 'Logger', console);  // Default to the logger instance initialized by the LSC LabShare package

        this._app = express();
        this._initialized = false;
        this._servicesActive = false;
        this._isProduction = this._app.get('env') === 'production';

        this._options = _.defaultsDeep(options, {
            logger,
            connections: _.get(global.LabShare, 'Config.services.Socket.Connections'),
            loadServices: true,
            pattern: '{src/api,api}/*.js',
            main: process.cwd(),
            directories: [],
            morgan: {
                enable: true,
                format: this._isProduction ? 'combined' : 'dev',
                options: {
                    // Workaround to add fluentD integration with the morgan logging library
                    stream: _.get(this.logger, 'stream.write')
                }
            },
            security: _.defaultsDeep(_.get(global.LabShare, 'Config.services.security', {}), {
                sessionOptions: {
                    secret: require('crypto').randomBytes(64).toString('hex'),
                    resave: false,
                    saveUninitialized: false,
                    name: 'sessionID',
                    cookie: {
                        httpOnly: true,
                        maxAge: 60 * 60 * 1000,      // 1 hour
                        secure: this._isProduction   // only allow SSL cookies in production by default
                    }
                },
                contentSecurityPolicy: false,
                hpkp: false,
                referrerPolicy: {
                    policy: 'no-referrer'
                },
                corsOptions: {
                    origin: '*',
                    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
                    preflightContinue: false
                }
            })
        });

        this.server = serverUtils.createServer(this._app, this._options.logger);
        this._apiLoader = new Loader(this._app, this._options);
        this._socketLoader = new SocketIOLoader(this.server, this._options);
    }

    /**
     * @description Load the services and assign middleware but do not start up the server or establish
     * any socket connections yet
     * @api
     */
    initialize() {
        this._initialized = true;

        this._app.use(helmet(this._options.security));
        this._app.use(bodyParser.json());
        this._app.use(bodyParser.urlencoded({extended: true}));
        this._app.use(cors(this._options.security.corsOptions));

        if (this._options.morgan.enable) {
            this._app.use(morgan(this._options.morgan.format, this._options.morgan.options));
        }

        this._app.use(cookieParser());

        // See: https://github.com/expressjs/session#cookiesecure
        if (this._isProduction) {
            this._app.set('trust proxy', 1);  // trust first proxy
        }

        this._app.use(require('express-session')(this._options.security.sessionOptions));

        this._socketLoader.initialize();
        this._apiLoader.initialize();
    }

    /**
     * @description Allows additional modifications to be made to the Express instance and routes before the services are started up.
     * @param {Function} func - A configuration function that receives all the API routes and the express app as arguments
     * @api
     */
    config(func) {
        if (!this._initialized) {
            this.initialize();
        }

        if (this._servicesActive) {
            throw new Error('You cannot modify the LabShare API services after starting up the server!');
        }

        func({
            services: this._apiLoader.services,
            sockets: this._socketLoader.getSockets(),
            app: this._app,
            io: this.io()
        });
    }

    /**
     * @description Starts the server and sets up socket connections
     * @returns {Object} The Node.js HTTP or HTTPS server
     * @api
     */
    start() {
        if (!this._initialized) {
            this.initialize();
        }

        // Run all the package API 'config' functions
        this._apiLoader.setConfig({
            apiLoader: this._apiLoader,
            app: this._app
        });

        this._servicesActive = true;

        serverUtils.startServer(this.server, this._options.logger);

        // add API routes and socket connections unless configured not to
        if (this._options.loadServices) {
            this._apiLoader.setAPIs(_.get(global.LabShare, 'Config.services.ServicePath') || '/');

            this._socketLoader.connect();
            this._socketLoader.on('error', error => {
                this._options.logger.error(error);
            });
            this._socketLoader.on('status', message => {
                this._options.logger.info(message);
            });

            // TODO: find a better way to expose the IO instance

            if (!_.get(global, 'LabShare.IO')) {
                _.set(global, 'LabShare.IO', this.io());
            }
        } else {
            this._options.logger.warn('API services are disabled because Service\'s `options.loadServices` is missing or set to false!');
        }

        return this.server;
    }

    io() {
        return this._socketLoader.getIO();
    }
}

module.exports = Services;

// if executed as a standalone script...
if (!module.parent) {
    let services = new Services();
    services.start();
}
<|endoftext|>


<commit_before>'use strict';

// ------------------------------------------------------------------------------------------ Dependencies

var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var nconf = require('nconf');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

// ------------------------------------------------------------------------------------------ Module definition

function Settings(opt) {
	EventEmitter.call(this);
	this.options = opt || {};

	if(_.isString(this.options)) {
	 	this.options = {
			path: this.options
		};
	} else if(_.isObject(this.options)) {
		this.options = _.assign({
			path: './settings.json',
			encoding: 'utf8'
		}, opt);
	} else {
		throw 'Invalid options provided';
	}
}

util.inherits(Settings, EventEmitter);

Settings.prototype.get = function(next) {
	var self = this;
	fs.readFile(self.options.path, self.options.encoding, function(err, content) {
		try {
			var config = nconf.get();
			var settings = content ? JSON.parse(content) : {};
			var output = _.assign(config, settings);

			if(output.localstoragepath && !path.isAbsolute(output.localstoragepath)) {
				var basedir = output.basedir || path.join(__dirname, '../');
				output.localstoragepath = path.resolve(basedir, output.localstoragepath);
			}

			next(null, output);
		} catch(err) {
			next(err, {});
		}
	});
};

Settings.prototype.push = function(settings, next) {
	var self = this;

	fs.readFile(self.options.path, self.options.encoding, function(err, content) {
		try {

			var config = require('../config.json');
			var current = content ? JSON.parse(content) : {};
			var defaults = _.defaultsDeep(current, config);

			settings = self.fixBooleanValues(defaults, settings);
			var output = _.defaultsDeep(settings, defaults);

			fs.writeFile(self.options.path, JSON.stringify(output), self.options.encoding, function(err) {
				self.emit('settings.push', err, output);
				next(err);
			});
		} catch(err) {
			next(err);
		}
	});
};

Settings.prototype.getInstance = function(opt) {
	return new Settings(opt);
};

Settings.prototype.fixBooleanValues = function(src, dest) {
	var self = this;
	_.forOwn(src, function(value, key) {
		if(_.isObject(value) && dest[key]) {
			dest[key] = self.fixBooleanValues(value, dest[key]);
		} else if(_.isBoolean(value) || value === 'true') {
			if(_.isUndefined(dest[key])) {
				dest[key] = false;
			}
		}
	});
	return dest;
}

// ------------------------------------------------------------------------------------------ Module Exposure

module.exports = new Settings();
<commit_msg>fixes #128 : Move all settings into a section - Fixing Code Climate issue<commit_after>'use strict';

// ------------------------------------------------------------------------------------------ Dependencies

var fs = require('fs');
var path = require('path');
var _ = require('lodash');
var nconf = require('nconf');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

// ------------------------------------------------------------------------------------------ Module definition

function Settings(opt) {
	EventEmitter.call(this);
	this.options = opt || {};

	if(_.isString(this.options)) {
	 	this.options = {
			path: this.options
		};
	} else if(_.isObject(this.options)) {
		this.options = _.assign({
			path: './settings.json',
			encoding: 'utf8'
		}, opt);
	} else {
		throw 'Invalid options provided';
	}
}

util.inherits(Settings, EventEmitter);

Settings.prototype.get = function(next) {
	var self = this;
	fs.readFile(self.options.path, self.options.encoding, function(err, content) {
		try {
			var config = nconf.get();
			var settings = content ? JSON.parse(content) : {};
			var output = _.assign(config, settings);

			if(output.localstoragepath && !path.isAbsolute(output.localstoragepath)) {
				var basedir = output.basedir || path.join(__dirname, '../');
				output.localstoragepath = path.resolve(basedir, output.localstoragepath);
			}

			next(null, output);
		} catch(err) {
			next(err, {});
		}
	});
};

Settings.prototype.push = function(settings, next) {
	var self = this;

	fs.readFile(self.options.path, self.options.encoding, function(err, content) {
		try {

			var config = require('../config.json');
			var current = content ? JSON.parse(content) : {};
			var defaults = _.defaultsDeep(current, config);

			settings = self.fixBooleanValues(defaults, settings);
			var output = _.defaultsDeep(settings, defaults);

			fs.writeFile(self.options.path, JSON.stringify(output), self.options.encoding, function(err) {
				self.emit('settings.push', err, output);
				next(err);
			});
		} catch(err) {
			next(err);
		}
	});
};

Settings.prototype.getInstance = function(opt) {
	return new Settings(opt);
};

Settings.prototype.fixBooleanValues = function(src, dest) {
	var self = this;
	_.forOwn(src, function(value, key) {
		if(_.isObject(value) && dest[key]) {
			dest[key] = self.fixBooleanValues(value, dest[key]);
		} else if(_.isBoolean(value) || value === 'true') {
			if(_.isUndefined(dest[key])) {
				dest[key] = false;
			}
		}
	});
	return dest;
};

// ------------------------------------------------------------------------------------------ Module Exposure

module.exports = new Settings();
<|endoftext|>


<commit_before>var mkdirp = require('mkdirp'),
	fs = require('fs'),
	path = require('path'),
	url = require('url'),
	async = require('async'),
	knox = require('knox'),
	crypto = require('crypto');

var Page = require('./Page'),
	Site = require('./Site'),
	File = require('./File'),
	fsUtil = require('./util/fs.js'),
	listBucket = require('./listBucket');

var hash = function(object, callback) {
	if (object.file)
		return hashFile(object.file, callback);
	callback(null, hashString(object.string));
};

var hashFile = function (file, callback) {
	var hash = crypto.createHash('md5');
	hash.setEncoding('hex');
	var readStream = fs.createReadStream(file);
	readStream.on('end', function() {
		hash.end();
		callback(null, hash.read());
	});
	readStream.pipe(hash);
};

var hashString = function(string) {
	return crypto.createHash('md5').update(string).digest('hex');
};

var exportSite = function(site, save, done) {
	var pageQueue = async.queue(function(page, done) {
		page.files.forEach(addToQueue);
		save(page, url.resolve(page.url, 'index.html'), done);
	}, 10);

	var queuePages = function(page) {
		// Filter out pages whose names consist only of dashes (e.g. used for
		// navigation spacers), since their URL parts would collapse and over-
		// ride parent pages.
		// TODO: Find a better fix.
		if (!/^-*$/.test(page.name)) {
			addToQueue(page);
		}
		for (var i = 0; i < page.children.length; i++)
			queuePages(page.children[i]);
		if (page.translatedCopies) {
			for (var lang in page.translatedCopies) {
				queuePages(page.translatedCopies[lang]);
			}
		}
	};

	var addToQueue = function(task) {
		if (task instanceof Page)
			return pageQueue.push(task);
		queue.push(task);
	};

	var queue = async.queue(function(task, done) {
		if (task instanceof File)
			return save({ file: task.uri }, path.relative(site.root.url, task.url), done);

		if (task.file)
			return save(task, path.relative(site.getUri(), task.file), done);

		fsUtil.recursiveDirectoryList(task.directory, function(err, found) {
			if (err) return done(err);
			found.files.forEach(function(file) {
				addToQueue({file: file});
			});
			done();
		});
	}, 10);

	var uploadAssets = function() {
		addToQueue({directory: site.getUri('assets')});
		queue.drain = done;
	};

	// When the page queue has drained, check if there are any thumbnails left
	// to be rendered:
	pageQueue.drain = function(err) {
		if (err) return callback(err);
		if (site.thumbnails.queueLength > 0) {
			console.log('Waiting for thumbnails to finish exporting...');
			return site.thumbnails.on('done', uploadAssets);
		}
		uploadAssets();
	};

	queuePages(site.root);
};

Site.prototype.publish = function(directory, callback) {
	var site = this;
	directory = directory ? directory : path.join(site.directory, 'out');
	var save = function(object, uri, callback) {
		var file = path.join(directory, uri),
			dir = path.dirname(file);

		mkdirp(dir, function() {
			if (object instanceof Page) {
				var html = object.getHtml();
				return fs.writeFile(file, html, callback);
			}
			fs.createReadStream(object.file).pipe(fs.createWriteStream(file)).on('close', callback);
		});
	};
	mkdirp(directory, function() {
		exportSite(site, save, callback);
	});
};

Site.prototype.syncS3 = function(callback) {
	// Try to read keys from environment if not specified in the settings
	this.settings.s3key = this.settings.s3key || process.env.AWS_ACCESS_KEY_ID;
	this.settings.s3secret = this.settings.s3secret || process.env.AWS_SECRET_ACCESS_KEY;

	if (!this.settings.s3key || !this.settings.s3secret || !this.settings.s3bucket)
		return callback(Error('S3 configuration not found. Missing one or more of s3key, s3secret and s3bucket settings in site-directory/settings.json'));
	var rootUrl = this.root.url,
		s3List = {};

	var client = knox.createClient({
		key: this.settings.s3key,
		secret: this.settings.s3secret,
		bucket: this.settings.s3bucket,
		region: this.settings.s3region || 'us-standard',
		secure: true
	});

	var listFiles = function(callback) {
		console.log('Getting S3 bucket list');
		listBucket(client, function(err, results) {
			if (err) return callback(err);
			results.forEach(function(result) {
				s3List[result.Key] = result.ETag;
			});
			callback();
		});
	};

	var put = function(object, uri, callback) {
		if (object instanceof Page)
			return putHtml(object, callback);
		var done = function() {
			delete s3List[uri];
			process.nextTick(callback);
		};
		hash(object, function(err, hash) {
			if (err) return callback(err);
			// Skip the file if the hashes match (note that Amazon
			// puts quotes around their ETags, so we have to do the same)
			if (s3List[uri] && s3List[uri] == '"' + hash + '"')
				return done();
			client.putFile(object.file, uri, { 'x-amz-acl': 'public-read' }, done);
		});
	};

	var putHtml = function(page, callback) {
		// FIXME: Does this work on Windows?
		var uri = path.join(path.relative(rootUrl, page.url), 'index.html');
		var html = page.getHtml();

		var s3Options = {
			'x-amz-acl': 'public-read',
			'Content-Type': 'text/html'
		};
		if (s3List[uri] && s3List[uri] == '"' + hashString(html) + '"') {
			delete s3List[uri];
			return callback();
		}
		client.putBuffer(html, uri, s3Options, callback);
	};

	var site = this;
	listFiles(function(err) {
		if (err) return callback(err);
		exportSite(site, put, function(err) {
			if (err) return callback(err);
			var toDelete = [];
			for (var uri in s3List)
				toDelete.push(uri);
			async.eachLimit(toDelete, 10, function(file, done) {
				client.deleteFile(file, done);
			}, callback);
		});
	});
};
<commit_msg>Fix three bugs with the s3 sync.<commit_after>"use strict";
var mkdirp = require('mkdirp'),
	fs = require('fs'),
	path = require('path'),
	url = require('url'),
	async = require('async'),
	knox = require('knox'),
	crypto = require('crypto');

var Page = require('./Page'),
	Site = require('./Site'),
	File = require('./File'),
	fsUtil = require('./util/fs.js'),
	listBucket = require('./listBucket');

var hashFile = function (file, callback) {
	var hash = crypto.createHash('md5');
	hash.setEncoding('hex');
	var readStream = fs.createReadStream(file.file);
	readStream.on('end', function() {
		hash.end();
		callback(null, hash.read());
	});
	readStream.pipe(hash);
};

var hashBuffer = function(buffer) {
	return crypto.createHash('md5').update(buffer).digest('hex');
};

var exportSite = function(site, save, done) {
	var pageQueue = async.queue(function(page, done) {
		page.files.forEach(addToQueue);
		save(page, url.resolve(page.url, 'index.html'), done);
	}, 10);

	var queuePages = function(page) {
		// Filter out pages whose names consist only of dashes (e.g. used for
		// navigation spacers), since their URL parts would collapse and over-
		// ride parent pages.
		// TODO: Find a better fix.
		if (!/^-*$/.test(page.name)) {
			addToQueue(page);
		}
		for (var i = 0; i < page.children.length; i++)
			queuePages(page.children[i]);
		if (page.translatedCopies) {
			for (var lang in page.translatedCopies) {
				queuePages(page.translatedCopies[lang]);
			}
		}
	};

	var addToQueue = function(task) {
		if (task instanceof Page)
			return pageQueue.push(task);
		queue.push(task);
	};

	var queue = async.queue(function(task, done) {
		if (task instanceof File)
			return save({ file: task.uri }, path.relative(site.root.url, task.url), done);

		if (task.file)
			return save(task, path.relative(site.getUri(), task.file), done);

		fsUtil.recursiveDirectoryList(task.directory, function(err, found) {
			if (err) return done(err);
			found.files.forEach(function(file) {
				addToQueue({file: file});
			});
			done();
		});
	}, 10);

	var uploadAssets = function() {
		addToQueue({directory: site.getUri('assets')});
		queue.drain = done;
	};

	// When the page queue has drained, check if there are any thumbnails left
	// to be rendered:
	pageQueue.drain = function(err) {
		if (err) return callback(err);
		if (site.thumbnails.queueLength > 0) {
			console.log('Waiting for thumbnails to finish exporting...');
			return site.thumbnails.on('done', uploadAssets);
		}
		uploadAssets();
	};

	queuePages(site.root);
};

Site.prototype.publish = function(directory, callback) {
	var site = this;
	directory = directory ? directory : path.join(site.directory, 'out');
	var save = function(object, uri, callback) {
		var file = path.join(directory, uri),
			dir = path.dirname(file);

		mkdirp(dir, function() {
			if (object instanceof Page) {
				var html = object.getHtml();
				return fs.writeFile(file, html, callback);
			}
			fs.createReadStream(object.file).pipe(fs.createWriteStream(file)).on('close', callback);
		});
	};
	mkdirp(directory, function() {
		exportSite(site, save, callback);
	});
};

Site.prototype.syncS3 = function(callback) {
	// Try to read keys from environment if not specified in the settings
	this.settings.s3key = this.settings.s3key || process.env.AWS_ACCESS_KEY_ID;
	this.settings.s3secret = this.settings.s3secret || process.env.AWS_SECRET_ACCESS_KEY;

	if (!this.settings.s3key || !this.settings.s3secret || !this.settings.s3bucket)
		return callback(Error('S3 configuration not found. Missing one or more of s3key, s3secret and s3bucket settings in site-directory/settings.json'));
	var rootUrl = this.root.url,
		s3List = {};

	var client = knox.createClient({
		key: this.settings.s3key,
		secret: this.settings.s3secret,
		bucket: this.settings.s3bucket,
		region: this.settings.s3region || 'us-standard',
		secure: true
	});

	var listFiles = function(callback) {
		console.log('Getting S3 bucket list');
		listBucket(client, function(err, results) {
			if (err) return callback(err);
			for (let file of results) {
				s3List[file.Key] = {
					hash: file.ETag,
					uploaded: false
				}
			}
			callback();
		});
	};

	var put = function(object, uri, callback) {
		if (object instanceof Page) {
			return putHtml(object, callback);
		}

		var done = function() {
			process.nextTick(callback);
		};

		hashFile(object, function(err, hash) {
			if (err) return callback(err);
			// Skip the file if the hashes match (note that Amazon
			// puts quotes around their ETags, so we have to do the same)
			if (s3List[uri] && s3List[uri].hash == '"' + hash + '"') {
				s3List[uri].uploaded = true;
				return done();
			} else {
				client.putFile(object.file, uri, { 'x-amz-acl': 'public-read' }, done);
			}
		});

	};

	var putHtml = function(page, callback) {
		// FIXME: Does this work on Windows?
		var uri = path.join(path.relative(rootUrl, page.url), 'index.html');
		var html = page.getHtml();

		var s3Options = {
			'x-amz-acl': 'public-read',
			'Content-Type': 'text/html'
		};

		let buffer = new Buffer(html);

		if (s3List[uri] && s3List[uri].hash == '"' + hashBuffer(buffer) + '"') {
			s3List[uri].uploaded = true;
			callback();
		} else {
			client.putBuffer(buffer, uri, s3Options, callback);
		}

	};

	var site = this;
	listFiles(function(err) {
		if (err) return callback(err);
		exportSite(site, put, function(err) {
			if (err) return callback(err);
			var toDelete = [];
			for (let uri in s3List) {
				if (!s3List[uri].uploaded) {
					toDelete.push(uri);
				}
			}
			async.eachLimit(toDelete, 10, function(file, done) {
				client.deleteFile(file, done);
			}, callback);
		});
	});
};
<|endoftext|>