<commit_before>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var crypto = require('crypto');
var http = require('http');
var Stream = require('stream').Stream;
var util = require('util');

var mime = require('mime');

var errors = require('./errors');
var Request = require('./request');

///--- Globals

var HttpError = errors.HttpError;
var RestError = errors.RestError;

var ALLOW_HEADERS = [
  'Accept',
  'Accept-Version',
  'Content-Length',
  'Content-MD5',
  'Content-Type',
  'Date',
  'X-Api-Version'
].join(', ');

var EXPOSE_HEADERS = [
  'X-Api-Version',
  'X-Request-Id'
].join(', ');

var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

var MONTHS = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec'
];

///--- Helpers

function httpDate(now) {
  if (!now)
    now = new Date();

  function pad(val) {
    if (parseInt(val, 10) < 10)
      val = '0' + val;
    return val;
  }

  return DAYS[now.getUTCDay()] + ', ' +
    pad(now.getUTCDate()) + ' ' +
    MONTHS[now.getUTCMonth()] + ' ' +
    now.getUTCFullYear() + ' ' +
    pad(now.getUTCHours()) + ':' +
    pad(now.getUTCMinutes()) + ':' +
    pad(now.getUTCSeconds()) + ' GMT';
}

function formatBinary(req, res, body) {
  if (!body)
    return null;

  if (!Buffer.isBuffer(body))
    body = new Buffer(body.toString());

  return body;
}

function formatText(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    body = body.message;
  } else if (typeof (body) === 'object') {
    body = JSON.stringify(body);
  } else {
    body = body.toString();
  }

  return body;
}

function formatJSON(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    if (body instanceof RestError) {
      body = body.body;
    } else {
      body = {
        message: body.message
      };
    }
  }

  if (Buffer.isBuffer(body))
    body = body.toString('base64');

  return JSON.stringify(body);
}

function ensureDefaultFormatters(res) {
  if (!res.formatters)
    res.formatters = {};

  //
  // We do this so that JSON formatters et al happen before custom ones,
  // like HTML (i.e., if your client is curl, accept defaults to * / *, and
  // v8 object keys are predictable, so we order them on the object such that
  // JSON goes first
  //

  var save = res.formatters || {};
  res.formatters = {};
  if (!save['application/json'])
    res.formatters['application/json'] = formatJSON;

  if (!save['text/plain'])
    res.formatters['text/plain'] = formatText;

  if (!save['application/octet-stream'])
    res.formatters['application/octet-stream'] = formatBinary;

  Object.keys(save).forEach(function (k) {
    if (!res.formatters[k])
      res.formatters[k] = save[k];
  });

  return this;
}

///--- API

/**
 * Response is a wrapper over node's http.OutgoingMessage object.
 *
 * Previous versions of restify just extended the prototype, but that was
 * problemtic if used in conjunction with other modules that do this as well
 * (namely express).
 *
 * This object exposes all of the properties/events/methods of the standard
 * Response object, plus some extra goodies.
 *
 * Note that at construct time, you pass in a map of mime type to formatter
 * functions. i.e:
 *
 * var res = new Response({
 *   formatters: {
 *     json: function (req, res, data, callback) {
 *       return callback(null, JSON.stringify(data));
 *     }
 *   }
 * });
 *
 * Note there is a special formatter called `error` which is invoked when
 * data is an error.
 *
 * @param {Object} options pass in bunyan handle, http.OutgoingMessage and a
 *                 Request.
 * @throws {TypeError} on input error.
 */
function Response(options) {
  if (typeof (options) !== 'object')
    throw new TypeError('options (Object) required');
  if (typeof (options.log) !== 'object')
    throw new TypeError('options.log (Object) required');
  if (typeof (options.response) !== 'object')
    throw new TypeError('options.response (http.OutgoingMessage) required');
  if (!(options.request instanceof Request))
    throw new TypeError('options.request (Request) required');
  if (options.version && typeof (options.version) !== 'string')
    throw new TypeError('options.version (String) required');

  var self = this;

  Stream.call(this);

  this.charSet = false;
  this._contentType = false;
  this._continueSent = false;
  this.formatters = {};
  this.log = options.request.log;
  this.methods = options.methods || [];
  this.req = options.request;
  this.res = options.response;
  this.serverName = options.serverName || 'restify';
  this.version = options.version || false;
  this.responseTimeHeader = options.responseTimeHeader || 'X-Response-Time';
  this.responseTimeFormatter = function(durationInMilliseconds) { return durationInMilliseconds; };

  // make sure the options is a function (if not we don't allow you to override)
  if (typeof(options.responseTimeFormatter) === 'function') {
    this.responseTimeFormatter = options.responseTimeFormatter;
  }

  // Generate the formatters
  if (options.formatters) {
    Object.keys(options.formatters).forEach(function (k) {
      if (k.indexOf('/') === -1)
        k = mime.lookup(k);

      self.formatters[k] = options.formatters[k];
    });
  }
  ensureDefaultFormatters(this);

  this.__defineSetter__('code', function (c) {
    if (typeof (c) !== 'number')
      throw new TypeError('code (Number) required');
    if (c < 100 || c > 599)
      throw new TypeError('code is not a valid HTTP status code');

    self.res.statusCode = c;
  });

  this.__defineGetter__('code', function () {
    return self.res.statusCode || 200;
  });

  this.__defineSetter__('contentLength', function (len) {
    if (typeof (len) !== 'number')
      throw new TypeError('contentLength must be Number');

    self.header('Content-Length', len);
  });

  this.__defineGetter__('contentLength', function () {
    return self.res.getHeader('Content-Length');
  });

  this.__defineSetter__('contentType', function (type) {
    if (typeof (type) !== 'string')
      throw new TypeError('contentType must be String');

    if (type.indexOf('/') === -1)
      type = mime.lookup(type);

    if (!self.charSet)
      self.charSet = mime.charsets.lookup(type, self.charSet);

    self._contentType = type;
  });

  this.__defineGetter__('contentType', function () {
    return self._contentType;
  });

  this.__defineGetter__('etag', function () {
    return self.get('etag');
  });

  this.__defineSetter__('etag', function (val) {
    return self.set('etag', val);
  });

  this.__defineGetter__('headers', function () {
    return self.res._headers || {};
  });

  this.__defineGetter__('id', function () {
    return self.req.requestId;
  });

  this.__defineGetter__('requestId', function () {
    return self.req.requestId;
  });

  this.__defineGetter__('statusCode', function () {
    return self.code;
  });

  this.__defineSetter__('statusCode', function (code) {
    self.code = code;
  });

  var types = Object.keys(this.formatters);
  this.__defineGetter__('types', function () {
    return types;
  });

  this.__defineGetter__('writable', function () {
    return self.res.writable;
  });

  this.__defineGetter__('writeable', function () {
    return self.res.writable;
  });

  ///--- Events
  this.res.on('drain', function () {
    return self.emit('drain');
  });

  this.res.on('error', function (err) {
    return self.emit('error', err);
  });

  this.res.on('close', function () {
    return self.emit('close');
  });

  this.res.on('pipe', function (src) {
    return self.emit('pipe', src);
  });

  this.res.on('finish', function (src) {
    return self.emit('finish', src);
  }
  // Finally write the default headers in
  this.defaultHeaders();
}
util.inherits(Response, Stream);
module.exports = Response;

Response.ACCEPTABLE = [
  'application/json',
  'text/plain',
  'application/octet-stream'
];

///--- Set up pass-throughs for all the APIs

Object.keys(http.OutgoingMessage.prototype).forEach(function (m) {
  Response.prototype[m] = function () {
    return this.res[m].apply(this.res, arguments);
  };
});

///--- http.OutgoingMessage APIs

Response.prototype.writeHead = function writeHead() {
  if (this.statusCode === 204 || this.statusCode === 304) {
    this.removeHeader('Content-Length');
    this.removeHeader('Content-MD5');
    this.removeHeader('Content-Type');
  }

  this.res.writeHead.apply(this.res, arguments);
  this.emit('header');
};

Response.prototype.writeContinue = function writeContinue() {
  return this.res.writeContinue.apply(this.res, arguments);
};

///-- Express APIs (that we want)

Response.prototype.header = function header(name, value) {
  if (typeof (name) !== 'string')
    throw new TypeError('name (String) required');

  if (value === undefined)
    return this.getHeader(name);

  if (value instanceof Date)
    value = httpDate(value);

  this.setHeader(name, value);
  return value;
};

Response.prototype.get = function get(name) {
  return this.getHeader(name);
};

Response.prototype.set = function set(name, val) {
  if (arguments.length === 2) {
    if (typeof (name) !== 'string')
      throw new TypeError('name (String) required');
    this.header(name, val);
  } else {
    if (typeof (name) !== 'object')
      throw new TypeError('Object required for argument 1');

    var self = this;
    Object.keys(name).forEach(function (k) {
      self.header(k, name[k]);
    });
  }

  return this;
};

Response.prototype.cache = function cache(type, options) {
  if (typeof (type) !== 'string') {
    options = type;
    type = 'public';
  }

  if (options && options.maxAge) {
    if (typeof (options.maxAge) !== 'number')
      throw new TypeError('options.maxAge (Number) required');
    type += ', max-age=' + (options.maxAge / 1000);
  }

  return this.header('Cache-Control', type);
};

Response.prototype.status = function status(code) {
  this.code = code;
  return this;
};

Response.prototype.defaultHeaders = function defaultHeaders() {
  this.header('Access-Control-Allow-Origin', '*');
  this.header('Access-Control-Allow-Headers', ALLOW_HEADERS);

  if (this.methods.length)
    this.header('Access-Control-Allow-Methods', this.methods.join());

  this.header('Access-Control-Expose-Headers', EXPOSE_HEADERS + ', ' + this.responseTimeHeader);
  this.header('Server', this.serverName);

  if (this.version)
    this.header('X-Api-Version', this.version);

  this.header('X-Request-Id', this.id);
};

Response.prototype.finishHeaders = function finishHeaders(data) {
  var now = new Date();

  if (!this.header('Connection') && !this.keepAlive)
    this.header('Connection', 'close');

  if (!this.header('Content-Length') && data) {
    if (Buffer.isBuffer(data)) {
      this.header('Content-Length', data.length);
    } else {
      this.header('Content-Length', Buffer.byteLength(data));
    }
  } else {
    this.header('Content-Length', 0);
  }

  if (!this.header('Content-MD5') && data) {
    var hash = crypto.createHash('md5');
    hash.update(data);
    this.header('Content-MD5', hash.digest('base64'));
  }

  if (!this.header('Content-Type') && data && this.contentType) {
    var type = this.contentType;
    if (this.charSet)
      type += '; charset=' + this.charSet;
    this.header('Content-Type', type);
  }

  if (!this.header('Date'))
    this.header('Date', httpDate(now));

  if (!this.header(this.responseTimeHeader))
    this.header(this.responseTimeHeader, this.responseTimeFormatter(now.getTime() - this.req.time.getTime()));
};

Response.prototype.send = function send(body) {
  assert.ok(this.methods);

  var head = (this.req.method === 'HEAD');

  switch (arguments.length) {
  case 0:
    body = null;
    break;

  case 1:
    if (typeof (body) === 'number') {
      this.statusCode = body;
      body = null;
    } else if (body && body instanceof HttpError) {
      this.statusCode = body.statusCode;
    }
    break;

  default:
    this.statusCode = body;
    body = arguments[1];
    break;
  }

  // the formatter might tack in Content- headers, so fill in headers after
  // serialization
  var data = body ? this.format(body) : null;
  this.finishHeaders(data);

  this.writeHead(this.statusCode);
  if (data && !head && this.statusCode !== 204 && this.statusCode !== 304)
    this.write(data);
  this.end();

  var self = this;
  this.log.trace({res: self}, 'response sent');
  return this;
};

Response.prototype.json = function json(obj) {
  if (arguments.length === 2) {
    this.code = obj;
    obj = arguments[1];
  }

  if (typeof (obj) !== 'object')
    throw new TypeError('object (Object) required');

  this.contentType = 'application/json';
  return this.send(obj);
};

Response.prototype.format = function format(body) {
  var log = this.log;

  log.trace('format entered(type=%s)', this.contentType);

  if (!this.contentType) {
    for (var i = 0; i < this.types.length; i++) {
      if (this.req.accepts(this.types[i])) {
        this.contentType = this.types[i];
        break;
      }
    }
  }

  if (this.types.indexOf(this.contentType) === -1) {
    this.contentType = 'application/octet-stream';
    log.trace('format content-type overriden: %s', this.contentType);
  }

  assert.ok(this.formatters[this.contentType]);

  if (body instanceof Error)
    this.code = (body instanceof errors.HttpError) ? body.statusCode : 500;

  var data = this.formatters[this.contentType](this.req, this, body);

  log.trace('format returing: %s', data);

  return data;
};

Response.prototype.toString = function toString() {
  var headers = '';
  var self = this;
  Object.keys(this.headers).forEach(function (k) {
    headers += k + ': ' + self.headers[k] + '\n';
  });
  return 'HTTP/1.1 ' + this.code + ' ' + http.STATUS_CODES[this.code] + '\n' +
    headers;
};
<commit_msg>Whoops, forgot that silly semicolon.<commit_after>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var crypto = require('crypto');
var http = require('http');
var Stream = require('stream').Stream;
var util = require('util');

var mime = require('mime');

var errors = require('./errors');
var Request = require('./request');

///--- Globals

var HttpError = errors.HttpError;
var RestError = errors.RestError;

var ALLOW_HEADERS = [
  'Accept',
  'Accept-Version',
  'Content-Length',
  'Content-MD5',
  'Content-Type',
  'Date',
  'X-Api-Version'
].join(', ');

var EXPOSE_HEADERS = [
  'X-Api-Version',
  'X-Request-Id'
].join(', ');

var DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

var MONTHS = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec'
];

///--- Helpers

function httpDate(now) {
  if (!now)
    now = new Date();

  function pad(val) {
    if (parseInt(val, 10) < 10)
      val = '0' + val;
    return val;
  }

  return DAYS[now.getUTCDay()] + ', ' +
    pad(now.getUTCDate()) + ' ' +
    MONTHS[now.getUTCMonth()] + ' ' +
    now.getUTCFullYear() + ' ' +
    pad(now.getUTCHours()) + ':' +
    pad(now.getUTCMinutes()) + ':' +
    pad(now.getUTCSeconds()) + ' GMT';
}

function formatBinary(req, res, body) {
  if (!body)
    return null;

  if (!Buffer.isBuffer(body))
    body = new Buffer(body.toString());

  return body;
}

function formatText(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    body = body.message;
  } else if (typeof (body) === 'object') {
    body = JSON.stringify(body);
  } else {
    body = body.toString();
  }

  return body;
}

function formatJSON(req, res, body) {
  if (!body)
    return null;

  if (body instanceof Error) {
    if (body instanceof RestError) {
      body = body.body;
    } else {
      body = {
        message: body.message
      };
    }
  }

  if (Buffer.isBuffer(body))
    body = body.toString('base64');

  return JSON.stringify(body);
}

function ensureDefaultFormatters(res) {
  if (!res.formatters)
    res.formatters = {};

  //
  // We do this so that JSON formatters et al happen before custom ones,
  // like HTML (i.e., if your client is curl, accept defaults to * / *, and
  // v8 object keys are predictable, so we order them on the object such that
  // JSON goes first
  //

  var save = res.formatters || {};
  res.formatters = {};
  if (!save['application/json'])
    res.formatters['application/json'] = formatJSON;

  if (!save['text/plain'])
    res.formatters['text/plain'] = formatText;

  if (!save['application/octet-stream'])
    res.formatters['application/octet-stream'] = formatBinary;

  Object.keys(save).forEach(function (k) {
    if (!res.formatters[k])
      res.formatters[k] = save[k];
  });

  return this;
}

///--- API

/**
 * Response is a wrapper over node's http.OutgoingMessage object.
 *
 * Previous versions of restify just extended the prototype, but that was
 * problemtic if used in conjunction with other modules that do this as well
 * (namely express).
 *
 * This object exposes all of the properties/events/methods of the standard
 * Response object, plus some extra goodies.
 *
 * Note that at construct time, you pass in a map of mime type to formatter
 * functions. i.e:
 *
 * var res = new Response({
 *   formatters: {
 *     json: function (req, res, data, callback) {
 *       return callback(null, JSON.stringify(data));
 *     }
 *   }
 * });
 *
 * Note there is a special formatter called `error` which is invoked when
 * data is an error.
 *
 * @param {Object} options pass in bunyan handle, http.OutgoingMessage and a
 *                 Request.
 * @throws {TypeError} on input error.
 */
function Response(options) {
  if (typeof (options) !== 'object')
    throw new TypeError('options (Object) required');
  if (typeof (options.log) !== 'object')
    throw new TypeError('options.log (Object) required');
  if (typeof (options.response) !== 'object')
    throw new TypeError('options.response (http.OutgoingMessage) required');
  if (!(options.request instanceof Request))
    throw new TypeError('options.request (Request) required');
  if (options.version && typeof (options.version) !== 'string')
    throw new TypeError('options.version (String) required');

  var self = this;

  Stream.call(this);

  this.charSet = false;
  this._contentType = false;
  this._continueSent = false;
  this.formatters = {};
  this.log = options.request.log;
  this.methods = options.methods || [];
  this.req = options.request;
  this.res = options.response;
  this.serverName = options.serverName || 'restify';
  this.version = options.version || false;
  this.responseTimeHeader = options.responseTimeHeader || 'X-Response-Time';
  this.responseTimeFormatter = function(durationInMilliseconds) { return durationInMilliseconds; };

  // make sure the options is a function (if not we don't allow you to override)
  if (typeof(options.responseTimeFormatter) === 'function') {
    this.responseTimeFormatter = options.responseTimeFormatter;
  }

  // Generate the formatters
  if (options.formatters) {
    Object.keys(options.formatters).forEach(function (k) {
      if (k.indexOf('/') === -1)
        k = mime.lookup(k);

      self.formatters[k] = options.formatters[k];
    });
  }
  ensureDefaultFormatters(this);

  this.__defineSetter__('code', function (c) {
    if (typeof (c) !== 'number')
      throw new TypeError('code (Number) required');
    if (c < 100 || c > 599)
      throw new TypeError('code is not a valid HTTP status code');

    self.res.statusCode = c;
  });

  this.__defineGetter__('code', function () {
    return self.res.statusCode || 200;
  });

  this.__defineSetter__('contentLength', function (len) {
    if (typeof (len) !== 'number')
      throw new TypeError('contentLength must be Number');

    self.header('Content-Length', len);
  });

  this.__defineGetter__('contentLength', function () {
    return self.res.getHeader('Content-Length');
  });

  this.__defineSetter__('contentType', function (type) {
    if (typeof (type) !== 'string')
      throw new TypeError('contentType must be String');

    if (type.indexOf('/') === -1)
      type = mime.lookup(type);

    if (!self.charSet)
      self.charSet = mime.charsets.lookup(type, self.charSet);

    self._contentType = type;
  });

  this.__defineGetter__('contentType', function () {
    return self._contentType;
  });

  this.__defineGetter__('etag', function () {
    return self.get('etag');
  });

  this.__defineSetter__('etag', function (val) {
    return self.set('etag', val);
  });

  this.__defineGetter__('headers', function () {
    return self.res._headers || {};
  });

  this.__defineGetter__('id', function () {
    return self.req.requestId;
  });

  this.__defineGetter__('requestId', function () {
    return self.req.requestId;
  });

  this.__defineGetter__('statusCode', function () {
    return self.code;
  });

  this.__defineSetter__('statusCode', function (code) {
    self.code = code;
  });

  var types = Object.keys(this.formatters);
  this.__defineGetter__('types', function () {
    return types;
  });

  this.__defineGetter__('writable', function () {
    return self.res.writable;
  });

  this.__defineGetter__('writeable', function () {
    return self.res.writable;
  });

  ///--- Events
  this.res.on('drain', function () {
    return self.emit('drain');
  });

  this.res.on('error', function (err) {
    return self.emit('error', err);
  });

  this.res.on('close', function () {
    return self.emit('close');
  });

  this.res.on('pipe', function (src) {
    return self.emit('pipe', src);
  });

  this.res.on('finish', function (src) {
    return self.emit('finish', src);
  });
  // Finally write the default headers in
  this.defaultHeaders();
}
util.inherits(Response, Stream);
module.exports = Response;

Response.ACCEPTABLE = [
  'application/json',
  'text/plain',
  'application/octet-stream'
];

///--- Set up pass-throughs for all the APIs

Object.keys(http.OutgoingMessage.prototype).forEach(function (m) {
  Response.prototype[m] = function () {
    return this.res[m].apply(this.res, arguments);
  };
});

///--- http.OutgoingMessage APIs

Response.prototype.writeHead = function writeHead() {
  if (this.statusCode === 204 || this.statusCode === 304) {
    this.removeHeader('Content-Length');
    this.removeHeader('Content-MD5');
    this.removeHeader('Content-Type');
  }

  this.res.writeHead.apply(this.res, arguments);
  this.emit('header');
};

Response.prototype.writeContinue = function writeContinue() {
  return this.res.writeContinue.apply(this.res, arguments);
};

///-- Express APIs (that we want)

Response.prototype.header = function header(name, value) {
  if (typeof (name) !== 'string')
    throw new TypeError('name (String) required');

  if (value === undefined)
    return this.getHeader(name);

  if (value instanceof Date)
    value = httpDate(value);

  this.setHeader(name, value);
  return value;
};

Response.prototype.get = function get(name) {
  return this.getHeader(name);
};

Response.prototype.set = function set(name, val) {
  if (arguments.length === 2) {
    if (typeof (name) !== 'string')
      throw new TypeError('name (String) required');
    this.header(name, val);
  } else {
    if (typeof (name) !== 'object')
      throw new TypeError('Object required for argument 1');

    var self = this;
    Object.keys(name).forEach(function (k) {
      self.header(k, name[k]);
    });
  }

  return this;
};

Response.prototype.cache = function cache(type, options) {
  if (typeof (type) !== 'string') {
    options = type;
    type = 'public';
  }

  if (options && options.maxAge) {
    if (typeof (options.maxAge) !== 'number')
      throw new TypeError('options.maxAge (Number) required');
    type += ', max-age=' + (options.maxAge / 1000);
  }

  return this.header('Cache-Control', type);
};

Response.prototype.status = function status(code) {
  this.code = code;
  return this;
};

Response.prototype.defaultHeaders = function defaultHeaders() {
  this.header('Access-Control-Allow-Origin', '*');
  this.header('Access-Control-Allow-Headers', ALLOW_HEADERS);

  if (this.methods.length)
    this.header('Access-Control-Allow-Methods', this.methods.join());

  this.header('Access-Control-Expose-Headers', EXPOSE_HEADERS + ', ' + this.responseTimeHeader);
  this.header('Server', this.serverName);

  if (this.version)
    this.header('X-Api-Version', this.version);

  this.header('X-Request-Id', this.id);
};

Response.prototype.finishHeaders = function finishHeaders(data) {
  var now = new Date();

  if (!this.header('Connection') && !this.keepAlive)
    this.header('Connection', 'close');

  if (!this.header('Content-Length') && data) {
    if (Buffer.isBuffer(data)) {
      this.header('Content-Length', data.length);
    } else {
      this.header('Content-Length', Buffer.byteLength(data));
    }
  } else {
    this.header('Content-Length', 0);
  }

  if (!this.header('Content-MD5') && data) {
    var hash = crypto.createHash('md5');
    hash.update(data);
    this.header('Content-MD5', hash.digest('base64'));
  }

  if (!this.header('Content-Type') && data && this.contentType) {
    var type = this.contentType;
    if (this.charSet)
      type += '; charset=' + this.charSet;
    this.header('Content-Type', type);
  }

  if (!this.header('Date'))
    this.header('Date', httpDate(now));

  if (!this.header(this.responseTimeHeader))
    this.header(this.responseTimeHeader, this.responseTimeFormatter(now.getTime() - this.req.time.getTime()));
};

Response.prototype.send = function send(body) {
  assert.ok(this.methods);

  var head = (this.req.method === 'HEAD');

  switch (arguments.length) {
  case 0:
    body = null;
    break;

  case 1:
    if (typeof (body) === 'number') {
      this.statusCode = body;
      body = null;
    } else if (body && body instanceof HttpError) {
      this.statusCode = body.statusCode;
    }
    break;

  default:
    this.statusCode = body;
    body = arguments[1];
    break;
  }

  // the formatter might tack in Content- headers, so fill in headers after
  // serialization
  var data = body ? this.format(body) : null;
  this.finishHeaders(data);

  this.writeHead(this.statusCode);
  if (data && !head && this.statusCode !== 204 && this.statusCode !== 304)
    this.write(data);
  this.end();

  var self = this;
  this.log.trace({res: self}, 'response sent');
  return this;
};

Response.prototype.json = function json(obj) {
  if (arguments.length === 2) {
    this.code = obj;
    obj = arguments[1];
  }

  if (typeof (obj) !== 'object')
    throw new TypeError('object (Object) required');

  this.contentType = 'application/json';
  return this.send(obj);
};

Response.prototype.format = function format(body) {
  var log = this.log;

  log.trace('format entered(type=%s)', this.contentType);

  if (!this.contentType) {
    for (var i = 0; i < this.types.length; i++) {
      if (this.req.accepts(this.types[i])) {
        this.contentType = this.types[i];
        break;
      }
    }
  }

  if (this.types.indexOf(this.contentType) === -1) {
    this.contentType = 'application/octet-stream';
    log.trace('format content-type overriden: %s', this.contentType);
  }

  assert.ok(this.formatters[this.contentType]);

  if (body instanceof Error)
    this.code = (body instanceof errors.HttpError) ? body.statusCode : 500;

  var data = this.formatters[this.contentType](this.req, this, body);

  log.trace('format returing: %s', data);

  return data;
};

Response.prototype.toString = function toString() {
  var headers = '';
  var self = this;
  Object.keys(this.headers).forEach(function (k) {
    headers += k + ': ' + self.headers[k] + '\n';
  });
  return 'HTTP/1.1 ' + this.code + ' ' + http.STATUS_CODES[this.code] + '\n' +
    headers;
};
<|endoftext|>


<commit_before>//response
//external modules
var ejs = require('ejs');
var fs = require('fs');
var path = require('path');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
var S = require('string');
var shortId = require('shortid');
var metaMarked = require('meta-marked');
var querystring = require('querystring');
var request = require('request');

//core
var config = require("./config.js");
var logger = require("./logger.js");
var models = require("./models");

//slides
var md = require('reveal.js/plugin/markdown/markdown');
var Mustache = require('mustache');

//reveal.js
var opts = {
    template: fs.readFileSync(config.slidepath).toString(),
    theme: 'css/theme/black.css',
    highlightTheme: 'zenburn',
    separator: '^(\r\n?|\n)---(\r\n?|\n)$',
    verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$',
    revealOptions: {}
};

//public
var response = {
    errorForbidden: function (res) {
        responseError(res, "403", "Forbidden", "oh no.");
    },
    errorNotFound: function (res) {
        responseError(res, "404", "Not Found", "oops.");
    },
    errorInternalError: function (res) {
        responseError(res, "500", "Internal Error", "wtf.");
    },
    errorServiceUnavailable: function (res) {
        res.status(503).send("I'm busy right now, try again later.");
    },
    newNote: newNote,
    showNote: showNote,
    showPublishNote: showPublishNote,
    showPublishSlide: showPublishSlide,
	showIndex: showIndex,
    noteActions: noteActions,
    publishNoteActions: publishNoteActions,
    githubActions: githubActions
};

function responseError(res, code, detail, msg) {
    res.writeHead(code, {
        'Content-Type': 'text/html'
    });
    var template = config.errorpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var content = compiled({
        url: config.serverurl,
        title: code + ' ' + detail + ' ' + msg,
        cache: !config.debug,
        filename: template,
        code: code,
        detail: detail,
        msg: msg,
		useCDN: config.usecdn
    });
    res.write(content);
    res.end();
}

function showIndex(req, res, next) {
    res.writeHead(200, {
        'Content-Type': 'text/html'
    });
    var template = config.indexpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var content = compiled({
        url: config.serverurl,
        useCDN: config.usecdn,
        facebook: config.facebook,
        twitter: config.twitter,
        github: config.github,
        gitlab: config.gitlab,
        dropbox: config.dropbox,
    });
    res.write(content);
    res.end();
}

function responseHackMD(res, note) {
    var body = LZString.decompressFromBase64(note.content);
    var meta = null;
    try {
        meta = metaMarked(body).meta;
    } catch(err) {
        //na
    }
    var title = models.Note.decodeTitle(note.title);
    title = models.Note.generateWebTitle(title);
    var template = config.hackmdpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var html = compiled({
        url: config.serverurl,
        title: title,
        useCDN: config.usecdn,
        robots: (meta && meta.robots) || false, //default allow robots
        facebook: config.facebook,
        twitter: config.twitter,
        github: config.github,
        gitlab: config.gitlab,
        dropbox: config.dropbox,
    });
    var buf = html;
    res.writeHead(200, {
        'Content-Type': 'text/html; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-Length': buf.length
    });
    res.end(buf);
}

function newNote(req, res, next) {
    var owner = null;
    if (req.isAuthenticated()) {
        owner = req.user.id;
    }
    models.Note.create({
        ownerId: owner
    }).then(function (note) {
        return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id));
    }).catch(function (err) {
        logger.error(err);
        return response.errorInternalError(res);
    });
}

function checkViewPermission(req, note) {
    if (note.permission == 'private') {
        if (!req.isAuthenticated() || note.ownerId != req.user.id)
            return false;
        else
            return true;
    } else {
        return true;
    }
}

function findNote(req, res, callback, include) {
    var id = req.params.noteId || req.params.shortid;
    models.Note.parseNoteId(id, function (err, _id) {
        models.Note.findOne({
            where: {
                id: _id
            },
            include: include || null
        }).then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            if (!checkViewPermission(req, note)) {
                return response.errorForbidden(res);
            } else {
                return callback(note);
            }
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    });
}

function showNote(req, res, next) {
    findNote(req, res, function (note) {
        return responseHackMD(res, note);
    });
}

function showPublishNote(req, res, next) {
    var include = [{
        model: models.User,
        as: "owner"
    }, {
        model: models.User,
        as: "lastchangeuser"
    }];
    findNote(req, res, function (note) {
        note.increment('viewcount').then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            var body = LZString.decompressFromBase64(note.content);
            var meta = null;
            try {
                meta = metaMarked(body).meta;
            } catch(err) {
                //na
            }
            var createtime = note.createdAt;
            var updatetime = note.lastchangeAt;
            var text = S(body).escapeHTML().s;
            var title = models.Note.decodeTitle(note.title);
            title = models.Note.generateWebTitle(title);
            var origin = config.serverurl;
            var data = {
                title: title,
                viewcount: note.viewcount,
                createtime: createtime,
                updatetime: updatetime,
                url: origin,
                body: text,
                useCDN: config.usecdn,
                lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
                robots: (meta && meta.robots) || false //default allow robots
            };
            return renderPublish(data, res);
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    }, include);
}

function renderPublish(data, res) {
    var template = config.prettypath;
    var options = {
        url: config.serverurl,
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var html = compiled(data);
    var buf = html;
    res.writeHead(200, {
        'Content-Type': 'text/html; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-Length': buf.length
    });
    res.end(buf);
}

function actionPublish(req, res, note) {
    res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid));
}

function actionSlide(req, res, note) {
    res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid));
}

function actionDownload(req, res, note) {
    var body = LZString.decompressFromBase64(note.content);
    var title = models.Note.decodeTitle(note.title);
    var filename = title;
    filename = encodeURIComponent(filename);
    res.writeHead(200, {
        'Access-Control-Allow-Origin': '*', //allow CORS as API
        'Access-Control-Allow-Headers': 'Range',
        'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
        'Content-Type': 'text/markdown; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-disposition': 'attachment; filename=' + filename + '.md',
        'Content-Length': body.length,
        'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
    });
    res.end(body);
}

function actionPDF(req, res, note) {
    var body = LZString.decompressFromBase64(note.content);
    try {
        body = metaMarked(body).markdown;
    } catch(err) {
        //na
    }
    var title = models.Note.decodeTitle(note.title);

    if (!fs.existsSync(config.tmppath)) {
        fs.mkdirSync(config.tmppath);
    }
    var path = config.tmppath + Date.now() + '.pdf';
    markdownpdf().from.string(body).to(path, function () {
        var stream = fs.createReadStream(path);
        var filename = title;
        // Be careful of special characters
        filename = encodeURIComponent(filename);
        // Ideally this should strip them
        res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
        res.setHeader('Cache-Control', 'private');
        res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
        res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
        stream.pipe(res);
        fs.unlink(path);
    });
}

function actionGist(req, res, note) {
    var data = {
        client_id: config.github.clientID,
        redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
        scope: "gist",
        state: shortId.generate()
    };
    var query = querystring.stringify(data);
    res.redirect("https://github.com/login/oauth/authorize?" + query);
}

function actionSnippet(req, res, note) {
    var data = {
        client_id: config.gitlab.clientID,
        redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
        scope: "snippet",
        state: shortId.generate()
    };
    var query = querystring.stringify(data);
    res.redirect(config.gitlab.baseURL + "/login/oauth/authorize?" + query);
}

function noteActions(req, res, next) {
    var noteId = req.params.noteId;
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "publish":
        case "pretty": //pretty deprecated
            actionPublish(req, res, note);
            break;
        case "slide":
            actionSlide(req, res, note);
            break;
        case "download":
            actionDownload(req, res, note);
            break;
        case "pdf":
            actionPDF(req, res, note);
            break;
        case "gist":
            actionGist(req, res, note);
            break;
        default:
            return res.redirect(config.serverurl + '/' + noteId);
            break;
        }
    });
}

function publishNoteActions(req, res, next) {
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "edit":
            res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
            break;
        default:
            res.redirect(config.serverurl + '/s/' + note.shortid);
            break;
        }
    });
}

function githubActions(req, res, next) {
    var noteId = req.params.noteId;
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "gist":
            githubActionGist(req, res, note);
            break;
        default:
            res.redirect(config.serverurl + '/' + noteId);
            break;
        }
    });
}

function gitlabActions(req, res, next) {
    var noteId = req.params.noteId;
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "gist":
            gitlabActionSnippet(req, res, note);
            break;
        default:
            res.redirect(config.serverurl + '/' + noteId);
            break;
        }
    });
}

function githubActionGist(req, res, note) {
    var code = req.query.code;
    var state = req.query.state;
    if (!code || !state) {
        return response.errorForbidden(res);
    } else {
        var data = {
            client_id: config.github.clientID,
            client_secret: config.github.clientSecret,
            code: code,
            state: state
        }
        var auth_url = 'https://github.com/login/oauth/access_token';
        request({
                url: auth_url,
                method: "POST",
                json: data
            }, function (error, httpResponse, body) {
            if (!error && httpResponse.statusCode == 200) {
                var access_token = body.access_token;
                if (access_token) {
                    var content = LZString.decompressFromBase64(note.content);
                    var title = models.Note.decodeTitle(note.title);
                    var filename = title.replace('/', ' ') + '.md';
                    var gist = {
                        "files": {}
                    };
                    gist.files[filename] = {
                        "content": content
                    };
                    var gist_url = "https://api.github.com/gists";
                    request({
                        url: gist_url,
                        headers: {
                            'User-Agent': 'HackMD',
                            'Authorization': 'token ' + access_token
                        },
                        method: "POST",
                        json: gist
                    }, function (error, httpResponse, body) {
                        if (!error && httpResponse.statusCode == 201) {
                            res.setHeader('referer', '');
                            res.redirect(body.html_url);
                        } else {
                            return response.errorForbidden(res);
                        }
                    });
                } else {
                    return response.errorForbidden(res);
                }
            } else {
                return response.errorForbidden(res);
            }
        })
    }
}

function gitlabActionSnippet(req, res, note) {
    var code = req.query.code;
    var state = req.query.state;
    if (!code || !state) {
        return response.errorForbidden(res);
    } else {
        var data = {
            client_id: config.gitlab.clientID,
            client_secret: config.gitlab.clientSecret,
            code: code,
            state: state
        }
        var auth_url = config.gitlab.baseURL + '/login/oauth/access_token';
        request({
                url: auth_url,
                method: "POST",
                json: data
            }, function (error, httpResponse, body) {
            if (!error && httpResponse.statusCode == 200) {
                var access_token = body.access_token;
                if (access_token) {
                    var content = LZString.decompressFromBase64(note.content);
                    var title = models.Note.decodeTitle(note.title);
                    var filename = title.replace('/', ' ') + '.md';
                    var gist = {
                        "files": {}
                    };
                    gist.files[filename] = {
                        "content": content
                    };
                    var gist_url = "https://api.gitlab.com/snippets";
                    request({
                        url: gist_url,
                        headers: {
                            'User-Agent': 'HackMD',
                            'Authorization': 'token ' + access_token
                        },
                        method: "POST",
                        json: gist
                    }, function (error, httpResponse, body) {
                        if (!error && httpResponse.statusCode == 201) {
                            res.setHeader('referer', '');
                            res.redirect(body.html_url);
                        } else {
                            return response.errorForbidden(res);
                        }
                    });
                } else {
                    return response.errorForbidden(res);
                }
            } else {
                return response.errorForbidden(res);
            }
        })
    }
}

function showPublishSlide(req, res, next) {
    findNote(req, res, function (note) {
        note.increment('viewcount').then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            var body = LZString.decompressFromBase64(note.content);
            try {
                body = metaMarked(body).markdown;
            } catch(err) {
                //na
            }
            var title = models.Note.decodeTitle(note.title);
            title = models.Note.generateWebTitle(title);
            var text = S(body).escapeHTML().s;
            render(res, title, text);
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    });
}

//reveal.js render
var render = function (res, title, markdown) {
    var slides = md.slidify(markdown, opts);

    res.end(Mustache.to_html(opts.template, {
        url: config.serverurl,
        title: title,
        theme: opts.theme,
        highlightTheme: opts.highlightTheme,
        slides: slides,
        options: JSON.stringify(opts.revealOptions, null, 2)
    }));
};

module.exports = response;<commit_msg>Remove skeleton functions<commit_after>//response
//external modules
var ejs = require('ejs');
var fs = require('fs');
var path = require('path');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
var S = require('string');
var shortId = require('shortid');
var metaMarked = require('meta-marked');
var querystring = require('querystring');
var request = require('request');

//core
var config = require("./config.js");
var logger = require("./logger.js");
var models = require("./models");

//slides
var md = require('reveal.js/plugin/markdown/markdown');
var Mustache = require('mustache');

//reveal.js
var opts = {
    template: fs.readFileSync(config.slidepath).toString(),
    theme: 'css/theme/black.css',
    highlightTheme: 'zenburn',
    separator: '^(\r\n?|\n)---(\r\n?|\n)$',
    verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$',
    revealOptions: {}
};

//public
var response = {
    errorForbidden: function (res) {
        responseError(res, "403", "Forbidden", "oh no.");
    },
    errorNotFound: function (res) {
        responseError(res, "404", "Not Found", "oops.");
    },
    errorInternalError: function (res) {
        responseError(res, "500", "Internal Error", "wtf.");
    },
    errorServiceUnavailable: function (res) {
        res.status(503).send("I'm busy right now, try again later.");
    },
    newNote: newNote,
    showNote: showNote,
    showPublishNote: showPublishNote,
    showPublishSlide: showPublishSlide,
	showIndex: showIndex,
    noteActions: noteActions,
    publishNoteActions: publishNoteActions,
    githubActions: githubActions
};

function responseError(res, code, detail, msg) {
    res.writeHead(code, {
        'Content-Type': 'text/html'
    });
    var template = config.errorpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var content = compiled({
        url: config.serverurl,
        title: code + ' ' + detail + ' ' + msg,
        cache: !config.debug,
        filename: template,
        code: code,
        detail: detail,
        msg: msg,
		useCDN: config.usecdn
    });
    res.write(content);
    res.end();
}

function showIndex(req, res, next) {
    res.writeHead(200, {
        'Content-Type': 'text/html'
    });
    var template = config.indexpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var content = compiled({
        url: config.serverurl,
        useCDN: config.usecdn,
        facebook: config.facebook,
        twitter: config.twitter,
        github: config.github,
        gitlab: config.gitlab,
        dropbox: config.dropbox,
    });
    res.write(content);
    res.end();
}

function responseHackMD(res, note) {
    var body = LZString.decompressFromBase64(note.content);
    var meta = null;
    try {
        meta = metaMarked(body).meta;
    } catch(err) {
        //na
    }
    var title = models.Note.decodeTitle(note.title);
    title = models.Note.generateWebTitle(title);
    var template = config.hackmdpath;
    var options = {
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var html = compiled({
        url: config.serverurl,
        title: title,
        useCDN: config.usecdn,
        robots: (meta && meta.robots) || false, //default allow robots
        facebook: config.facebook,
        twitter: config.twitter,
        github: config.github,
        gitlab: config.gitlab,
        dropbox: config.dropbox,
    });
    var buf = html;
    res.writeHead(200, {
        'Content-Type': 'text/html; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-Length': buf.length
    });
    res.end(buf);
}

function newNote(req, res, next) {
    var owner = null;
    if (req.isAuthenticated()) {
        owner = req.user.id;
    }
    models.Note.create({
        ownerId: owner
    }).then(function (note) {
        return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id));
    }).catch(function (err) {
        logger.error(err);
        return response.errorInternalError(res);
    });
}

function checkViewPermission(req, note) {
    if (note.permission == 'private') {
        if (!req.isAuthenticated() || note.ownerId != req.user.id)
            return false;
        else
            return true;
    } else {
        return true;
    }
}

function findNote(req, res, callback, include) {
    var id = req.params.noteId || req.params.shortid;
    models.Note.parseNoteId(id, function (err, _id) {
        models.Note.findOne({
            where: {
                id: _id
            },
            include: include || null
        }).then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            if (!checkViewPermission(req, note)) {
                return response.errorForbidden(res);
            } else {
                return callback(note);
            }
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    });
}

function showNote(req, res, next) {
    findNote(req, res, function (note) {
        return responseHackMD(res, note);
    });
}

function showPublishNote(req, res, next) {
    var include = [{
        model: models.User,
        as: "owner"
    }, {
        model: models.User,
        as: "lastchangeuser"
    }];
    findNote(req, res, function (note) {
        note.increment('viewcount').then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            var body = LZString.decompressFromBase64(note.content);
            var meta = null;
            try {
                meta = metaMarked(body).meta;
            } catch(err) {
                //na
            }
            var createtime = note.createdAt;
            var updatetime = note.lastchangeAt;
            var text = S(body).escapeHTML().s;
            var title = models.Note.decodeTitle(note.title);
            title = models.Note.generateWebTitle(title);
            var origin = config.serverurl;
            var data = {
                title: title,
                viewcount: note.viewcount,
                createtime: createtime,
                updatetime: updatetime,
                url: origin,
                body: text,
                useCDN: config.usecdn,
                lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
                robots: (meta && meta.robots) || false //default allow robots
            };
            return renderPublish(data, res);
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    }, include);
}

function renderPublish(data, res) {
    var template = config.prettypath;
    var options = {
        url: config.serverurl,
        cache: !config.debug,
        filename: template
    };
    var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
    var html = compiled(data);
    var buf = html;
    res.writeHead(200, {
        'Content-Type': 'text/html; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-Length': buf.length
    });
    res.end(buf);
}

function actionPublish(req, res, note) {
    res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid));
}

function actionSlide(req, res, note) {
    res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid));
}

function actionDownload(req, res, note) {
    var body = LZString.decompressFromBase64(note.content);
    var title = models.Note.decodeTitle(note.title);
    var filename = title;
    filename = encodeURIComponent(filename);
    res.writeHead(200, {
        'Access-Control-Allow-Origin': '*', //allow CORS as API
        'Access-Control-Allow-Headers': 'Range',
        'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
        'Content-Type': 'text/markdown; charset=UTF-8',
        'Cache-Control': 'private',
        'Content-disposition': 'attachment; filename=' + filename + '.md',
        'Content-Length': body.length,
        'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
    });
    res.end(body);
}

function actionPDF(req, res, note) {
    var body = LZString.decompressFromBase64(note.content);
    try {
        body = metaMarked(body).markdown;
    } catch(err) {
        //na
    }
    var title = models.Note.decodeTitle(note.title);

    if (!fs.existsSync(config.tmppath)) {
        fs.mkdirSync(config.tmppath);
    }
    var path = config.tmppath + Date.now() + '.pdf';
    markdownpdf().from.string(body).to(path, function () {
        var stream = fs.createReadStream(path);
        var filename = title;
        // Be careful of special characters
        filename = encodeURIComponent(filename);
        // Ideally this should strip them
        res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
        res.setHeader('Cache-Control', 'private');
        res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
        res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
        stream.pipe(res);
        fs.unlink(path);
    });
}

function actionGist(req, res, note) {
    var data = {
        client_id: config.github.clientID,
        redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
        scope: "gist",
        state: shortId.generate()
    };
    var query = querystring.stringify(data);
    res.redirect("https://github.com/login/oauth/authorize?" + query);
}

function noteActions(req, res, next) {
    var noteId = req.params.noteId;
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "publish":
        case "pretty": //pretty deprecated
            actionPublish(req, res, note);
            break;
        case "slide":
            actionSlide(req, res, note);
            break;
        case "download":
            actionDownload(req, res, note);
            break;
        case "pdf":
            actionPDF(req, res, note);
            break;
        case "gist":
            actionGist(req, res, note);
            break;
        default:
            return res.redirect(config.serverurl + '/' + noteId);
            break;
        }
    });
}

function publishNoteActions(req, res, next) {
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "edit":
            res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
            break;
        default:
            res.redirect(config.serverurl + '/s/' + note.shortid);
            break;
        }
    });
}

function githubActions(req, res, next) {
    var noteId = req.params.noteId;
    findNote(req, res, function (note) {
        var action = req.params.action;
        switch (action) {
        case "gist":
            githubActionGist(req, res, note);
            break;
        default:
            res.redirect(config.serverurl + '/' + noteId);
            break;
        }
    });
}

function githubActionGist(req, res, note) {
    var code = req.query.code;
    var state = req.query.state;
    if (!code || !state) {
        return response.errorForbidden(res);
    } else {
        var data = {
            client_id: config.github.clientID,
            client_secret: config.github.clientSecret,
            code: code,
            state: state
        }
        var auth_url = 'https://github.com/login/oauth/access_token';
        request({
                url: auth_url,
                method: "POST",
                json: data
            }, function (error, httpResponse, body) {
            if (!error && httpResponse.statusCode == 200) {
                var access_token = body.access_token;
                if (access_token) {
                    var content = LZString.decompressFromBase64(note.content);
                    var title = models.Note.decodeTitle(note.title);
                    var filename = title.replace('/', ' ') + '.md';
                    var gist = {
                        "files": {}
                    };
                    gist.files[filename] = {
                        "content": content
                    };
                    var gist_url = "https://api.github.com/gists";
                    request({
                        url: gist_url,
                        headers: {
                            'User-Agent': 'HackMD',
                            'Authorization': 'token ' + access_token
                        },
                        method: "POST",
                        json: gist
                    }, function (error, httpResponse, body) {
                        if (!error && httpResponse.statusCode == 201) {
                            res.setHeader('referer', '');
                            res.redirect(body.html_url);
                        } else {
                            return response.errorForbidden(res);
                        }
                    });
                } else {
                    return response.errorForbidden(res);
                }
            } else {
                return response.errorForbidden(res);
            }
        })
    }
}

function showPublishSlide(req, res, next) {
    findNote(req, res, function (note) {
        note.increment('viewcount').then(function (note) {
            if (!note) {
                return response.errorNotFound(res);
            }
            var body = LZString.decompressFromBase64(note.content);
            try {
                body = metaMarked(body).markdown;
            } catch(err) {
                //na
            }
            var title = models.Note.decodeTitle(note.title);
            title = models.Note.generateWebTitle(title);
            var text = S(body).escapeHTML().s;
            render(res, title, text);
        }).catch(function (err) {
            logger.error(err);
            return response.errorInternalError(res);
        });
    });
}

//reveal.js render
var render = function (res, title, markdown) {
    var slides = md.slidify(markdown, opts);

    res.end(Mustache.to_html(opts.template, {
        url: config.serverurl,
        title: title,
        theme: opts.theme,
        highlightTheme: opts.highlightTheme,
        slides: slides,
        options: JSON.stringify(opts.revealOptions, null, 2)
    }));
};

module.exports = response;<|endoftext|>


<commit_before>var request = require('request'),
    crypto = require('crypto');

exports.defaultRegion = 'us';
exports.privateKey = null;
exports.publicKey = null;

// Makes request
function get(options, callback) {
    // Default values
    options.region = options.region || exports.defaultRegion;
    options.game = options.game || 'wow';
    options.query = options.query ? '?' + options.query : '';

    var path = '/api/' + options.game + options.path,

        uri = encodeURI('http://' + options.region + '.battle.net' +
            path + options.query),

        headers = { 'Connection': 'keep-alive' };

    // Authentication
    if (exports.privateKey && exports.publicKey) {
        var signature = crypto.createHmac('sha1', exports.privateKey);

        signature.update(
            'GET\n' +
            new Date().toUTCString() + '\n' +
            path + '\n'
        );

        headers['Authorization'] = 'BNET ' + exports.publicKey + ':' +
            signature.digest('base64');
    }

    request({ uri: uri, headers: headers }, function(err, res, body) {
        if (err) {
            return callback(err);
        }

        try {
            body = JSON.parse(body);
        } catch (e) {
            return callback(e);
        }

        if (body.status === 'nok') {
            return callback(new Error(body.reason));
        }

        callback(null, body);
    });
}

// Requests one or more API resources
function resource(method, options, callback) {
    var names;

    // If single resource provided
    if (typeof options === 'string') {
        names = options;
        options = {};
    } else if (options.names) {
        names = options.names;
    } else {
        return callback(new Error('No character name(s) provided.'));
    }

    if (Array.isArray(options.fields)) {
        options.query = 'fields=' + options.fields.join(',');
    }

    options.path = [method, options.realm, options.size];

    var parse = function(name) {
        var parseOptions = {
            path: options.path.slice(0),
            query: options.query,
            region: options.region
        };

        // If path elements passed with name
        if (name.indexOf('_') !== -1) {
            name = name.split('_').reverse();

            parseOptions.path.push(name.pop());

            if (name[0] && name[0].length > 3) {
                parseOptions.path[1] = name.shift();
            }

            if (name[0] && name[0].length === 3) {
                parseOptions.path[2] = name.shift();
            }
        } else if (options.realm) {
            parseOptions.path.push(name);
        } else {
            return callback(new Error('No realm provided for ' + name + '.'));
        }

        // Remove empty path elements
        parseOptions.path = '/' + parseOptions.path.filter(function(el) {
            return !!el;
        }).join('/');

        get(parseOptions, callback);
    };

    if (Array.isArray(names)) {
        names.forEach(parse);
    } else {
        parse(names);
    }
}

// Realm Status API
exports.realmStatus = function() {
    var args = Array.prototype.slice.call(arguments),
        callback = args.pop(),
        options = { path: '/realm/status' };

    if (Array.isArray(args[0])) {
        args[0] = args[0].join('&realm=');
    }

    if (args[0] && args[0].length > 2) {
        options.query = 'realm=' + args.shift();
    }

    if (args[0] && args[0].length === 2) {
        options.region = args[0];
    }

    get(options, function(err, response) {
        if (err) {
            return callback(err);
        }

        callback(null, response.realms);
    });
};

// Item API
exports.item = function() {
    var args = Array.prototype.slice.call(arguments),
        callback = args.pop(),
        options = { path: '/data/item/' };

    if (args[1] && args[1].length === 2) {
        options.region = args[1];
    }

    var parse = function(id) {
        get({ path: options.path + id, region: options.region }, callback);
    };

    if (Array.isArray(args[0])) {
        args[0].forEach(parse);
    } else {
        parse(args[0]);
    }
};

// Exports resource API using generic function
['character', 'guild', 'arena'].forEach(function(method) {
    exports[method] = function(options, callback) {
        resource(method, options, callback);
    };
});

// Static data pages
['perks', 'rewards', 'classes', 'races'].forEach(function(method) {
    var path;

    // Add appropriate prefix
    switch (method) {
        case 'perks':
        case 'rewards':
            path = 'guild/' + method;
            break;
        case 'classes':
        case 'races':
            path = 'character/' + method;
            break;
    }

    path = '/data/' + path;

    exports[method] = function() {
        var args = Array.prototype.slice.call(arguments),
            callback = args.pop(),
            options = { path: path };

        if (args[0]) {
            options.region = args[0];
        }

        get(options, function(err, res) {
            if (err) {
                return callback(err);
            }

            callback(null, res[method]);
        });
    };
});
<commit_msg>Fixing error<commit_after>var request = require('request'),
    crypto = require('crypto');

exports.defaultRegion = 'us';
exports.privateKey = null;
exports.publicKey = null;

// Makes request
function get(options, callback) {
    // Default values
    options.region = options.region || exports.defaultRegion;
    options.game = options.game || 'wow';
    options.query = options.query ? '?' + options.query : '';

    var path = '/api/' + options.game + options.path,

        uri = encodeURI('http://' + options.region + '.battle.net' +
            path + options.query),

        headers = { 'Connection': 'keep-alive' };

    // Authentication
    if (exports.privateKey && exports.publicKey) {
        var signature = crypto.createHmac('sha1', exports.privateKey);

        signature.update(
            'GET\n' +
            new Date().toUTCString() + '\n' +
            path + '\n'
        );

        headers['Authorization'] = 'BNET ' + exports.publicKey + ':' +
            signature.digest('base64');
    }

    request({ uri: uri, headers: headers }, function(err, res, body) {
        if (err) {
            return callback(err);
        }

        try {
            body = JSON.parse(body);
        } catch (e) {
            return callback(e);
        }

        if (body.status === 'nok') {
            return callback(new Error(body.reason));
        }

        callback(null, body);
    });
}

// Requests one or more API resources
function resource(method, options, callback) {
    var names;

    // If single resource provided
    if (typeof options === 'string') {
        names = options;
        options = {};
    } else if (options.names) {
        names = options.names;
    } else {
        return callback(new Error('No name(s) provided.'));
    }

    if (Array.isArray(options.fields)) {
        options.query = 'fields=' + options.fields.join(',');
    }

    options.path = [method, options.realm, options.size];

    var parse = function(name) {
        var parseOptions = {
            path: options.path.slice(0),
            query: options.query,
            region: options.region
        };

        // If path elements passed with name
        if (name.indexOf('_') !== -1) {
            name = name.split('_').reverse();

            parseOptions.path.push(name.pop());

            if (name[0] && name[0].length > 3) {
                parseOptions.path[1] = name.shift();
            }

            if (name[0] && name[0].length === 3) {
                parseOptions.path[2] = name.shift();
            }
        } else if (options.realm) {
            parseOptions.path.push(name);
        } else {
            return callback(new Error('No realm provided for ' + name + '.'));
        }

        // Remove empty path elements
        parseOptions.path = '/' + parseOptions.path.filter(function(el) {
            return !!el;
        }).join('/');

        get(parseOptions, callback);
    };

    if (Array.isArray(names)) {
        names.forEach(parse);
    } else {
        parse(names);
    }
}

// Realm Status API
exports.realmStatus = function() {
    var args = Array.prototype.slice.call(arguments),
        callback = args.pop(),
        options = { path: '/realm/status' };

    if (Array.isArray(args[0])) {
        args[0] = args[0].join('&realm=');
    }

    if (args[0] && args[0].length > 2) {
        options.query = 'realm=' + args.shift();
    }

    if (args[0] && args[0].length === 2) {
        options.region = args[0];
    }

    get(options, function(err, response) {
        if (err) {
            return callback(err);
        }

        callback(null, response.realms);
    });
};

// Item API
exports.item = function() {
    var args = Array.prototype.slice.call(arguments),
        callback = args.pop(),
        options = { path: '/data/item/' };

    if (args[1] && args[1].length === 2) {
        options.region = args[1];
    }

    var parse = function(id) {
        get({ path: options.path + id, region: options.region }, callback);
    };

    if (Array.isArray(args[0])) {
        args[0].forEach(parse);
    } else {
        parse(args[0]);
    }
};

// Exports resource API using generic function
['character', 'guild', 'arena'].forEach(function(method) {
    exports[method] = function(options, callback) {
        resource(method, options, callback);
    };
});

// Static data pages
['perks', 'rewards', 'classes', 'races'].forEach(function(method) {
    var path;

    // Add appropriate prefix
    switch (method) {
        case 'perks':
        case 'rewards':
            path = 'guild/' + method;
            break;
        case 'classes':
        case 'races':
            path = 'character/' + method;
            break;
    }

    path = '/data/' + path;

    exports[method] = function() {
        var args = Array.prototype.slice.call(arguments),
            callback = args.pop(),
            options = { path: path };

        if (args[0]) {
            options.region = args[0];
        }

        get(options, function(err, res) {
            if (err) {
                return callback(err);
            }

            callback(null, res[method]);
        });
    };
});
<|endoftext|>


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

var contentDisposition = require('content-disposition');
var ensureErrorHandler = require('error-inject');
var getType = require('mime-types').contentType;
var onFinish = require('on-finished');
var isJSON = require('koa-is-json');
var escape = require('escape-html');
var typeis = require('type-is').is;
var statuses = require('statuses');
var destroy = require('destroy');
var assert = require('assert');
var path = require('path');
var vary = require('vary');
var basename = path.basename;
var extname = path.extname;

/**
 * Prototype.
 */

module.exports = {

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

  get socket() {
    // TODO: TLS
    return this.ctx.req.socket;
  },

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

  get header() {
    // TODO: wtf
    return this.res._headers || {};
  },

  /**
   * 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) {
    assert('number' == typeof code, 'status code must be a number');
    assert(statuses[code], 'invalid status code: ' + code);
    this._explicitStatus = true;
    this.res.statusCode = code;
    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) {
    var original = this._body;
    this._body = val;

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

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

    // set the content-type only if not yet set
    var setType = !this.header['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, this.ctx.onerror);

      // 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() {
    var len = this.header['content-length'];
    var body = this.body;

    if (null == len) {
      if (!body) return;
      if ('string' == typeof body) return Buffer.byteLength(body);
      if (Buffer.isBuffer(body)) return body.length;
      if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
      return;
    }

    return ~~len;
  },

  /**
   * 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: function(field){
    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: function(url, alt){
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', 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 ' + url + '.';
      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: function(filename){
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename));
  },

  /**
   * 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) {
    this.set('Content-Type', getType(type) || 'application/octet-stream');
  },

  /**
   * 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() {
    var 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 request mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

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

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

  is: function(types){
    var type = this.type;
    if (!types) return type || false;
    if (!Array.isArray(types)) types = [].slice.call(arguments);
    return typeis(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: function(field){
    return this.header[field.toLowerCase()];
  },

  /**
   * 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: function(field, val){
    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(String);
      else val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (var 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: function(field, val){
    var prev = this.get(field);
    var value = val;
    if (prev) {
      // concat the new and prev vals
      value = Array.isArray(prev) ? prev.concat(val)
        : Array.isArray(val) ? [prev].concat(val)
        : [prev, val];
    }

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

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

  remove: function(field){
    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() {
    var socket = this.res.socket;
    if (!socket) return false;
    return socket.writable;
  },

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

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

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

  toJSON: function(){
    return {
      status: this.status,
      message: this.message,
      header: this.header
    }
  }
};
<commit_msg>refactor res.append()<commit_after>
/**
 * Module dependencies.
 */

var contentDisposition = require('content-disposition');
var ensureErrorHandler = require('error-inject');
var getType = require('mime-types').contentType;
var onFinish = require('on-finished');
var isJSON = require('koa-is-json');
var escape = require('escape-html');
var typeis = require('type-is').is;
var statuses = require('statuses');
var destroy = require('destroy');
var assert = require('assert');
var path = require('path');
var vary = require('vary');
var basename = path.basename;
var extname = path.extname;

/**
 * Prototype.
 */

module.exports = {

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

  get socket() {
    // TODO: TLS
    return this.ctx.req.socket;
  },

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

  get header() {
    // TODO: wtf
    return this.res._headers || {};
  },

  /**
   * 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) {
    assert('number' == typeof code, 'status code must be a number');
    assert(statuses[code], 'invalid status code: ' + code);
    this._explicitStatus = true;
    this.res.statusCode = code;
    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) {
    var original = this._body;
    this._body = val;

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

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

    // set the content-type only if not yet set
    var setType = !this.header['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, this.ctx.onerror);

      // 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() {
    var len = this.header['content-length'];
    var body = this.body;

    if (null == len) {
      if (!body) return;
      if ('string' == typeof body) return Buffer.byteLength(body);
      if (Buffer.isBuffer(body)) return body.length;
      if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
      return;
    }

    return ~~len;
  },

  /**
   * 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: function(field){
    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: function(url, alt){
    // location
    if ('back' == url) url = this.ctx.get('Referrer') || alt || '/';
    this.set('Location', 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 ' + url + '.';
      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: function(filename){
    if (filename) this.type = extname(filename);
    this.set('Content-Disposition', contentDisposition(filename));
  },

  /**
   * 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) {
    this.set('Content-Type', getType(type) || 'application/octet-stream');
  },

  /**
   * 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() {
    var 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 request mime type void of
   * parameters such as "charset".
   *
   * @return {String}
   * @api public
   */

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

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

  is: function(types){
    var type = this.type;
    if (!types) return type || false;
    if (!Array.isArray(types)) types = [].slice.call(arguments);
    return typeis(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: function(field){
    return this.header[field.toLowerCase()];
  },

  /**
   * 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: function(field, val){
    if (2 == arguments.length) {
      if (Array.isArray(val)) val = val.map(String);
      else val = String(val);
      this.res.setHeader(field, val);
    } else {
      for (var 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: function(field, val){
    var 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: function(field){
    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() {
    var socket = this.res.socket;
    if (!socket) return false;
    return socket.writable;
  },

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

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

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

  toJSON: function(){
    return {
      status: this.status,
      message: this.message,
      header: this.header
    }
  }
};
<|endoftext|>


<commit_before>var rigging = require('rigging');

/**
* Recompile all LESS and SASS assets, then generate link tags for CSS files
* Either one minified file (in production), or a link to each individual file (in development)
*/
exports.css = function(cb) {
	var html = '';

	if(sails.config.environment === 'production') {
		// In production mode, use minified version built ahead of time
		html+='<link rel="stylesheet" type="text/css" media="all" href="/rigging_production/rigging.min.css"/>';
	}
	else {
		// In development mode, lookup css files on the rigging path on the fly
		var cssFiles = rigging.ls(sails.config.rigging.sequence,/\.css$/,true);

		// Add LESS and SASS files (these were compiled in res.view)
		cssFiles.push("rigging.less.css");
		cssFiles.push("rigging.sass.css");

		_.each(cssFiles,function(path) {
			html+='<link rel="stylesheet" type="text/css" media="all" href="/rigging_static/'+path+'"/>';
		});
	}

	return html;
};

/**
* Recompile all coffeescript assets, then generate script tags for js files
* Either one minified file (in production), or a link to each individual file (in development)
*/
exports.js = function(cb) {
	var html = '';

	// TODO: bundle and automatically include socket.io.js in the public directory
	html += '';

	if(sails.config.environment === 'production') {
		// In production mode, use minified version built ahead of time
		html += '';
	}
	else {
		// In development mode, lookup css files in the rigging dir(s) on the fly
		var jsFiles = rigging.ls(sails.config.rigging.sequence,/\.js$/,true);

		// Add CoffeeScript file (this was compiled in res.view)
		jsFiles.push("rigging.coffee.js");

		_.each(jsFiles,function(path) {
			html+='';
		});
	}
	return html;
};

/**
* Write templates to the template library div.
*
	* TODO: In lieu of a true cache, store the compiled templates in memory in production mode.
	* (Because templates are dumped directly into the layout, we cannot use standard HTTP or file caching methods.)
*/
exports.templateLibrary = function(cb) {
	var html = '
\n';

	// Get all template files in rigging sequence
	var templateFiles = rigging.ls(sails.config.rigging.sequence,/\.ejs$/);
	_.each(templateFiles,function(filepath) {
		html += fs.readFileSync(filepath,'utf8') + "\n";
	});

	html+="
";
	return html;
};<commit_msg>Removed automatic bundling of socket.io client (now included in Mast)<commit_after>var rigging = require('rigging');

/**
* Recompile all LESS and SASS assets, then generate link tags for CSS files
* Either one minified file (in production), or a link to each individual file (in development)
*/
exports.css = function(cb) {
	var html = '';

	if(sails.config.environment === 'production') {
		// In production mode, use minified version built ahead of time
		html+='<link rel="stylesheet" type="text/css" media="all" href="/rigging_production/rigging.min.css"/>';
	}
	else {
		// In development mode, lookup css files on the rigging path on the fly
		var cssFiles = rigging.ls(sails.config.rigging.sequence,/\.css$/,true);

		// Add LESS and SASS files (these were compiled in res.view)
		cssFiles.push("rigging.less.css");
		cssFiles.push("rigging.sass.css");

		_.each(cssFiles,function(path) {
			html+='<link rel="stylesheet" type="text/css" media="all" href="/rigging_static/'+path+'"/>';
		});
	}

	return html;
};

/**
* Recompile all coffeescript assets, then generate script tags for js files
* Either one minified file (in production), or a link to each individual file (in development)
*/
exports.js = function(cb) {
	var html = '';

	if(sails.config.environment === 'production') {
		// In production mode, use minified version built ahead of time
		html += '';
	}
	else {
		// In development mode, lookup css files in the rigging dir(s) on the fly
		var jsFiles = rigging.ls(sails.config.rigging.sequence,/\.js$/,true);

		// Add CoffeeScript file (this was compiled in res.view)
		jsFiles.push("rigging.coffee.js");

		_.each(jsFiles,function(path) {
			html+='';
		});
	}
	return html;
};

/**
* Write templates to the template library div.
*
	* TODO: In lieu of a true cache, store the compiled templates in memory in production mode.
	* (Because templates are dumped directly into the layout, we cannot use standard HTTP or file caching methods.)
*/
exports.templateLibrary = function(cb) {
	var html = '
\n';

	// Get all template files in rigging sequence
	var templateFiles = rigging.ls(sails.config.rigging.sequence,/\.ejs$/);
	_.each(templateFiles,function(filepath) {
		html += fs.readFileSync(filepath,'utf8') + "\n";
	});

	html+="
";
	return html;
};<|endoftext|>