<commit_before>var KBucket = require('k-bucket')
  , uuid = require('uuid')
  , events = require('events')
  , inherits = require('inherits')
  , ht = require("ht")
  , Network = require('./network')
  , protocol = require('./protocol')
  , bufferEqual = require('buffer-equal');

var defaultOptions = {
	networkPort: 2015,
	maxClosestNodes: 2,
	lookupTimeout: 5,         // seconds
	directConnectTimeout: 5, // seconds
	pingInterval: 30          // seconds
};

var Node = module.exports = function(options) {
	var that = this;

	if (!(this instanceof Node)) {
		return new Node(options);
	}

	this.options = options || {};

	for(var k in defaultOptions) {
		if ('undefined' === typeof this.options[k]) {
			this.options[k] = defaultOptions[k];
		} else {
			this.options[k] = options[k];
		}
	}

	this.localNodeContact = {
		id: new Buffer(uuid.v1()),
		ip: require('my-local-ip')(),
		port: this.options.networkPort
	}

	this.routeBucket = new KBucket({
		localNodeId: this.localNodeContact.id
	});

	this.messageList = new ht();
	this.unavailableNodeList = new ht();

	events.EventEmitter.call(this);

	this._setupNetwork();
};
inherits(Node, events.EventEmitter);

Node.prototype.lookup = function(nodeId, callback) {
	var that = this;

	var nodeIdBin = new Buffer(nodeId);

	if (KBucket.distance(nodeIdBin, this.routeBucket.localNodeId) === 0) {
		callback(true, this.localNodeContact);
		return;
	}

	var lookupNode = this.routeBucket.get(nodeIdBin);
	if (lookupNode) {
		callback(true, lookupNode, this.localNodeContact);
		return;
	}

	lookupNode = {id: nodeIdBin};
	var closestNodes = this.routeBucket.closest(lookupNode, this.options.maxClosestNodes);
	if (closestNodes.length === 0) {
		callback(false);
		return;
	}

	if (this.unavailableNodeList.contains(nodeId)) {
		callback(false);
		return;
	}

	console.log('lookup begin: nodeId ' + nodeId);

	var lookupFailTimeout = setTimeout(function() {
		console.log('lookup failed: nodeId ' + nodeId);
		callback(false);

		that.unavailableNodeList.put(nodeId, setTimeout(function() {
			that.unavailableNodeList.remove(nodeId);
		}, that.options.maxClosestNodes * that.options.lookupTimeout * 1000));
	}, that.options.lookupTimeout * 1000);

	var sendLookupMessage = function(node) {
		if (KBucket.distance(node.id, that.routeBucket.localNodeId) === 0) {
			return;
		}

		var messageId = that._sendLookupMessage(nodeId, node);

		var messageHandlers = {
			type: protocol.typeCodes['mate_lookup'],
			findHandler: function(respNodeArray) {
				clearTimeout(messageHandlers.timeoutHandler);
				that.messageList.remove(messageId);

				var index = that._indexOfNodeArray(nodeIdBin, respNodeArray);
				if (index >= 0) {
					if (lookupFailTimeout) {
						clearTimeout(lookupFailTimeout);
						lookupFailTimeout = null;

						callback(true, respNodeArray[index], node);
					}
				} else {
					if (!that.unavailableNodeList.contains(nodeId)) {
						respNodeArray.forEach(sendLookupMessage);
					}
				}
			},
			timeoutHandler: setTimeout(function() {
				console.log('lookup timeout: messageId ' + messageId +
					' nodeId ' + nodeId +
					' routeNodeId ' + node.id.toString());
				that.messageList.remove(messageId);
			}, that.options.lookupTimeout * 1000)
		};

		that.messageList.put(messageId, messageHandlers);
	};

	closestNodes.forEach(sendLookupMessage);
};

Node.prototype.directConnect = function(nodeAddress, callback) {
	var that = this;

	var messageId = this._sendConnectMessage(nodeAddress);

	var messageHandlers = {
		type: protocol.typeCodes['mate_connect'],
		conackHandler: function(nodeId, remoteAddress) {
			clearTimeout(messageHandlers.timeoutHandler);
			that.messageList.remove(messageId);

			var nodeIdBin = new Buffer(nodeId);
			var existNode = that.routeBucket.get(nodeIdBin);
			if (!existNode) {
				that._addNode(nodeIdBin, remoteAddress);
				callback(true);
			} else {
				console.log('exist node: ' + nodeId);
				callback(false);
			}
		},
		timeoutHandler: setTimeout(function() {
			console.log('directConnect timeout: messageId ' + messageId +
				' nodeIp ' + nodeAddress.ip + ' nodePort ' + nodeAddress.port);
			that.messageList.remove(messageId);
			callback(false);
		}, that.options.directConnectTimeout * 1000)
	};

	that.messageList.put(messageId, messageHandlers);
};

Node.prototype.end = function() {
	this.network.close();
};

Node.prototype._setupNetwork = function() {
	var that = this;

	this.network = new Network(this.options);

	this.network.on('listening', this.emit.bind(this, 'listening'));

	this.network.on('net_error', this.emit.bind(this, 'net_error'));
	this.network.on('net_close', this.emit.bind(this, 'net_close'));

	this.network.on('mate_lookup', function(message, address) {
		that._handleLookupMessage(message, address);
	});
	this.network.on('mate_lookresp', function(message, address) {
		that._handleLookrespMessage(message, address);
	});
	this.network.on('mate_connect', function(message, address) {
		that._handleConnectMessage(message, address);
	});
	this.network.on('mate_conack', function(message, address) {
		that._handleConackMessage(message, address);
	});
	this.network.on('mate_ping', function(message, address) {
		that._handlePingMessage(message, address);
	});
};

Node.prototype._sendLookupMessage = function(lookupNodeId, remoteNode) {
	var lookupMessage = {
		type: protocol.typeCodes['mate_lookup'],
		messageId: uuid.v1(),
		lookupNodeId: lookupNodeId
	};
	this.network.sendMessage(lookupMessage, remoteNode.address);
	return lookupMessage.messageId;
};

Node.prototype._handleLookupMessage = function(message, address) {
	if (!message.lookupNodeId || !message.messageId) {
		console.log('lookup message missing parameters');
		return;
	}

	var lookupNode = {id: new Buffer(message.lookupNodeId)};
	var closestNodes = this.routeBucket.closest(lookupNode, this.options.maxClosestNodes);

	var lookrespMessage = {
		type: protocol.typeCodes['mate_lookresp'],
		messageId: message.messageId,
		closestNodes: this._serializeNodeArray(closestNodes)
	};
	this.network.sendMessage(lookrespMessage, address);
};

Node.prototype._handleLookrespMessage = function(message, address) {
	if (!message.closestNodes || message.closestNodes.constructor !== Array) {
		console.log('lookresp message missing parameters');
		return;
	}

	if (!this.messageList.contains(message.messageId)) {
		console.log('unexpected message id: ' + message.messageId);
		return;
	}

	var messageHandlers = this.messageList.get(message.messageId);
	messageHandlers.findHandler(this._unserializeNodeArray(message.closestNodes));
};

Node.prototype._sendConnectMessage = function(remoteAddress) {
	var connectMessage = {
		type: protocol.typeCodes['mate_connect'],
		messageId: uuid.v1(),
		nodeId: this.localNodeContact.id.toString()
	};
	this.network.sendMessage(connectMessage, remoteAddress);
	return connectMessage.messageId;
};

Node.prototype._handleConnectMessage = function(message, address) {
	if (!message.nodeId || !message.messageId) {
		console.log('connect message missing parameters');
		return;
	}

	var nodeIdBin = new Buffer(message.nodeId);
	var existNode = this.routeBucket.get(nodeIdBin);
	if (!existNode) {
		this._addNode(nodeIdBin, address);
	}

	var conackMessage = {
		type: protocol.typeCodes['mate_conack'],
		messageId: message.messageId,
		nodeId: this.localNodeContact.id.toString()
	};
	this.network.sendMessage(conackMessage, address);
};

Node.prototype._handleConackMessage = function(message, address) {
	if (!message.nodeId) {
		console.log('conack message missing parameters');
		return;
	}

	if (!this.messageList.contains(message.messageId)) {
		console.log('unexpected message id: ' + message.messageId);
		return;
	}

	var messageHandlers = this.messageList.get(message.messageId);
	messageHandlers.conackHandler(message.nodeId, address);
};

Node.prototype._sendPingMessage = function(nodeIdBin, remoteAddress) {
	var pingMessage = {
		type: protocol.typeCodes['mate_ping'],
		nodeId: nodeIdBin.toString()
	};
	this.network.sendMessage(pingMessage, remoteAddress);
};

Node.prototype._handlePingMessage = function(message, address) {
	if (!message.nodeId) {
		console.log('ping message missing parameters');
		return;
	}

	var nodeIdBin = new Buffer(message.nodeId);
	var contact = this.routeBucket.get(nodeIdBin);
	if (contact) {
		contact.lastActive = Date.now();
	}
};

Node.prototype._indexOfNodeArray = function(nodeIdBin, nodeArray) {
    for (var i = 0; i < nodeArray.length; i++) {
        if (bufferEqual(nodeArray[i].id, nodeIdBin)) {
        	return i;
        }
    }
    return -1;
};

Node.prototype._serializeNodeArray = function(nodeArray) {
	var serializedNodeArray = [];
	for (var i = 0; i < nodeArray.length; i++) {
		serializedNodeArray[i] = {
			id: nodeArray[i].id.toString(),
			address: nodeArray[i].address
		};
	}

	return serializedNodeArray;
};

Node.prototype._unserializeNodeArray = function(serializedNodeArray) {
	var nodeArray = [];
	for (var i = 0; i < serializedNodeArray.length; i++) {
		nodeArray[i] = {
			id: new Buffer(serializedNodeArray[i].id),
			address: serializedNodeArray[i].address
		};
	}

	return nodeArray;
};

Node.prototype._addNode = function(nodeIdBin, nodeAddress) {
	console.log('add node ' + nodeIdBin.toString() + ' ip ' + nodeAddress.ip + ' port ' + nodeAddress.port);

	var that = this;
	var contact = {
		id: nodeIdBin,
		address: nodeAddress,
		lastActive: Date.now(),
		pingInterval: setInterval(function() {
			that._sendPingMessage(contact.id, contact.address);
		}, this.options.pingInterval * 1000),
		checkInterval: setInterval(function() {
			if (Date.now() - contact.lastActive > 2 * that.options.pingInterval * 1000) {
				console.log('node ' + contact.id.toString() + ' timeout');
				clearInterval(contact.pingInterval);
				clearInterval(contact.checkInterval);
				that.routeBucket.remove(contact);
			}
		}, 2 * this.options.pingInterval * 1000)
	};
	this.routeBucket.add(contact);
};<commit_msg>fix ping message with wrong nodeid<commit_after>var KBucket = require('k-bucket')
  , uuid = require('uuid')
  , events = require('events')
  , inherits = require('inherits')
  , ht = require("ht")
  , Network = require('./network')
  , protocol = require('./protocol')
  , bufferEqual = require('buffer-equal');

var defaultOptions = {
	networkPort: 2015,
	maxClosestNodes: 2,
	lookupTimeout: 5,         // seconds
	directConnectTimeout: 5, // seconds
	pingInterval: 30          // seconds
};

var Node = module.exports = function(options) {
	var that = this;

	if (!(this instanceof Node)) {
		return new Node(options);
	}

	this.options = options || {};

	for(var k in defaultOptions) {
		if ('undefined' === typeof this.options[k]) {
			this.options[k] = defaultOptions[k];
		} else {
			this.options[k] = options[k];
		}
	}

	this.localNodeContact = {
		id: new Buffer(uuid.v1()),
		ip: require('my-local-ip')(),
		port: this.options.networkPort
	}

	this.routeBucket = new KBucket({
		localNodeId: this.localNodeContact.id
	});

	this.messageList = new ht();
	this.unavailableNodeList = new ht();

	events.EventEmitter.call(this);

	this._setupNetwork();
};
inherits(Node, events.EventEmitter);

Node.prototype.lookup = function(nodeId, callback) {
	var that = this;

	var nodeIdBin = new Buffer(nodeId);

	if (KBucket.distance(nodeIdBin, this.routeBucket.localNodeId) === 0) {
		callback(true, this.localNodeContact);
		return;
	}

	var lookupNode = this.routeBucket.get(nodeIdBin);
	if (lookupNode) {
		callback(true, lookupNode, this.localNodeContact);
		return;
	}

	lookupNode = {id: nodeIdBin};
	var closestNodes = this.routeBucket.closest(lookupNode, this.options.maxClosestNodes);
	if (closestNodes.length === 0) {
		callback(false);
		return;
	}

	if (this.unavailableNodeList.contains(nodeId)) {
		callback(false);
		return;
	}

	console.log('lookup begin: nodeId ' + nodeId);

	var lookupFailTimeout = setTimeout(function() {
		console.log('lookup failed: nodeId ' + nodeId);
		callback(false);

		that.unavailableNodeList.put(nodeId, setTimeout(function() {
			that.unavailableNodeList.remove(nodeId);
		}, that.options.maxClosestNodes * that.options.lookupTimeout * 1000));
	}, that.options.lookupTimeout * 1000);

	var sendLookupMessage = function(node) {
		if (KBucket.distance(node.id, that.routeBucket.localNodeId) === 0) {
			return;
		}

		var messageId = that._sendLookupMessage(nodeId, node);

		var messageHandlers = {
			type: protocol.typeCodes['mate_lookup'],
			findHandler: function(respNodeArray) {
				clearTimeout(messageHandlers.timeoutHandler);
				that.messageList.remove(messageId);

				var index = that._indexOfNodeArray(nodeIdBin, respNodeArray);
				if (index >= 0) {
					if (lookupFailTimeout) {
						clearTimeout(lookupFailTimeout);
						lookupFailTimeout = null;

						callback(true, respNodeArray[index], node);
					}
				} else {
					if (!that.unavailableNodeList.contains(nodeId)) {
						respNodeArray.forEach(sendLookupMessage);
					}
				}
			},
			timeoutHandler: setTimeout(function() {
				console.log('lookup timeout: messageId ' + messageId +
					' nodeId ' + nodeId +
					' routeNodeId ' + node.id.toString());
				that.messageList.remove(messageId);
			}, that.options.lookupTimeout * 1000)
		};

		that.messageList.put(messageId, messageHandlers);
	};

	closestNodes.forEach(sendLookupMessage);
};

Node.prototype.directConnect = function(nodeAddress, callback) {
	var that = this;

	var messageId = this._sendConnectMessage(nodeAddress);

	var messageHandlers = {
		type: protocol.typeCodes['mate_connect'],
		conackHandler: function(nodeId, remoteAddress) {
			clearTimeout(messageHandlers.timeoutHandler);
			that.messageList.remove(messageId);

			var nodeIdBin = new Buffer(nodeId);
			var existNode = that.routeBucket.get(nodeIdBin);
			if (!existNode) {
				that._addNode(nodeIdBin, remoteAddress);
				callback(true);
			} else {
				console.log('exist node: ' + nodeId);
				callback(false);
			}
		},
		timeoutHandler: setTimeout(function() {
			console.log('directConnect timeout: messageId ' + messageId +
				' nodeIp ' + nodeAddress.ip + ' nodePort ' + nodeAddress.port);
			that.messageList.remove(messageId);
			callback(false);
		}, that.options.directConnectTimeout * 1000)
	};

	that.messageList.put(messageId, messageHandlers);
};

Node.prototype.end = function() {
	this.network.close();
};

Node.prototype._setupNetwork = function() {
	var that = this;

	this.network = new Network(this.options);

	this.network.on('listening', this.emit.bind(this, 'listening'));

	this.network.on('net_error', this.emit.bind(this, 'net_error'));
	this.network.on('net_close', this.emit.bind(this, 'net_close'));

	this.network.on('mate_lookup', function(message, address) {
		that._handleLookupMessage(message, address);
	});
	this.network.on('mate_lookresp', function(message, address) {
		that._handleLookrespMessage(message, address);
	});
	this.network.on('mate_connect', function(message, address) {
		that._handleConnectMessage(message, address);
	});
	this.network.on('mate_conack', function(message, address) {
		that._handleConackMessage(message, address);
	});
	this.network.on('mate_ping', function(message, address) {
		that._handlePingMessage(message, address);
	});
};

Node.prototype._sendLookupMessage = function(lookupNodeId, remoteNode) {
	var lookupMessage = {
		type: protocol.typeCodes['mate_lookup'],
		messageId: uuid.v1(),
		lookupNodeId: lookupNodeId
	};
	this.network.sendMessage(lookupMessage, remoteNode.address);
	return lookupMessage.messageId;
};

Node.prototype._handleLookupMessage = function(message, address) {
	if (!message.lookupNodeId || !message.messageId) {
		console.log('lookup message missing parameters');
		return;
	}

	var lookupNode = {id: new Buffer(message.lookupNodeId)};
	var closestNodes = this.routeBucket.closest(lookupNode, this.options.maxClosestNodes);

	var lookrespMessage = {
		type: protocol.typeCodes['mate_lookresp'],
		messageId: message.messageId,
		closestNodes: this._serializeNodeArray(closestNodes)
	};
	this.network.sendMessage(lookrespMessage, address);
};

Node.prototype._handleLookrespMessage = function(message, address) {
	if (!message.closestNodes || message.closestNodes.constructor !== Array) {
		console.log('lookresp message missing parameters');
		return;
	}

	if (!this.messageList.contains(message.messageId)) {
		console.log('unexpected message id: ' + message.messageId);
		return;
	}

	var messageHandlers = this.messageList.get(message.messageId);
	messageHandlers.findHandler(this._unserializeNodeArray(message.closestNodes));
};

Node.prototype._sendConnectMessage = function(remoteAddress) {
	var connectMessage = {
		type: protocol.typeCodes['mate_connect'],
		messageId: uuid.v1(),
		nodeId: this.localNodeContact.id.toString()
	};
	this.network.sendMessage(connectMessage, remoteAddress);
	return connectMessage.messageId;
};

Node.prototype._handleConnectMessage = function(message, address) {
	if (!message.nodeId || !message.messageId) {
		console.log('connect message missing parameters');
		return;
	}

	var nodeIdBin = new Buffer(message.nodeId);
	var existNode = this.routeBucket.get(nodeIdBin);
	if (!existNode) {
		this._addNode(nodeIdBin, address);
	}

	var conackMessage = {
		type: protocol.typeCodes['mate_conack'],
		messageId: message.messageId,
		nodeId: this.localNodeContact.id.toString()
	};
	this.network.sendMessage(conackMessage, address);
};

Node.prototype._handleConackMessage = function(message, address) {
	if (!message.nodeId) {
		console.log('conack message missing parameters');
		return;
	}

	if (!this.messageList.contains(message.messageId)) {
		console.log('unexpected message id: ' + message.messageId);
		return;
	}

	var messageHandlers = this.messageList.get(message.messageId);
	messageHandlers.conackHandler(message.nodeId, address);
};

Node.prototype._sendPingMessage = function(remoteAddress) {
	var pingMessage = {
		type: protocol.typeCodes['mate_ping'],
		nodeId: this.localNodeContact.id.toString()
	};
	this.network.sendMessage(pingMessage, remoteAddress);
};

Node.prototype._handlePingMessage = function(message, address) {
	if (!message.nodeId) {
		console.log('ping message missing parameters');
		return;
	}

	var nodeIdBin = new Buffer(message.nodeId);
	var contact = this.routeBucket.get(nodeIdBin);
	if (contact) {
		contact.lastActive = Date.now();
	} else {
		console.log('ping message: unknown node ' + message.nodeId);
	}
};

Node.prototype._indexOfNodeArray = function(nodeIdBin, nodeArray) {
    for (var i = 0; i < nodeArray.length; i++) {
        if (bufferEqual(nodeArray[i].id, nodeIdBin)) {
        	return i;
        }
    }
    return -1;
};

Node.prototype._serializeNodeArray = function(nodeArray) {
	var serializedNodeArray = [];
	for (var i = 0; i < nodeArray.length; i++) {
		serializedNodeArray[i] = {
			id: nodeArray[i].id.toString(),
			address: nodeArray[i].address
		};
	}

	return serializedNodeArray;
};

Node.prototype._unserializeNodeArray = function(serializedNodeArray) {
	var nodeArray = [];
	for (var i = 0; i < serializedNodeArray.length; i++) {
		nodeArray[i] = {
			id: new Buffer(serializedNodeArray[i].id),
			address: serializedNodeArray[i].address
		};
	}

	return nodeArray;
};

Node.prototype._addNode = function(nodeIdBin, nodeAddress) {
	console.log('add node ' + nodeIdBin.toString() + ' ip ' + nodeAddress.ip + ' port ' + nodeAddress.port);

	var that = this;
	var contact = {
		id: nodeIdBin,
		address: nodeAddress,
		lastActive: Date.now(),
		pingInterval: setInterval(function() {
			that._sendPingMessage(contact.address);
		}, this.options.pingInterval * 1000),
		checkInterval: setInterval(function() {
			if (Date.now() - contact.lastActive > 2 * that.options.pingInterval * 1000) {
				console.log('node ' + contact.id.toString() + ' timeout');
				clearInterval(contact.pingInterval);
				clearInterval(contact.checkInterval);
				that.routeBucket.remove(contact);
			}
		}, 2 * this.options.pingInterval * 1000)
	};
	this.routeBucket.add(contact);
};<|endoftext|>


<commit_before>var assert = require('assert');
var Q = require('q');
var fs = require('fs');
var url = require('url');
var request = require('request');
var PassThrough = require('stream').PassThrough;
var extend = require('util')._extend;
var util = require('./util');
var engine = require('./engine');
var dataMgr = require('./data-mgr');
var ONE_SEC = 1000;
var outerList = [];
var slice = Array.prototype.slice;
var TYPE_ATTR = typeof Symbol == 'function' ? Symbol['#vase-type'] : '#vase-type/' + Math.random();
var locals;

function setType(param, type) {
	param[TYPE_ATTR] = type || true;
	return param;
}

function getType(param) {
	return param && param[TYPE_ATTR];
}

function resolveValue(param) {
	if (getType(param)) {
		param = param();
	}

	if (Q.isPromise(param)) {
		return param;
	}

	if (Array.isArray(param)) {
		return Q.all(param);
	}

	var deferred = Q.defer();
	deferred.resolve(param || '');
	return deferred.promise;
}

function resolveValues(params) {

	return Q.all(params.map(function(param) {
		return getType(param) ? param() : (param == null ? '' : param);
	}));
}

function out(data, delay, speed) {
	var delay = delay && parseInt(delay, 10) || 0;
	var speed = speed && parseFloat(speed, 10) || 0;

	outerList.push([data, delay, speed, getType(data)]);
}

function render(tpl, locals, engineType) {

	return setType(function() {
		var deferred = Q.defer();
		if (typeof tpl == 'string' || typeof  tpl == 'number') {
			tpl += '';
			var _tpl = dataMgr.getTpl(tpl);
			if (_tpl) {
				tpl = _tpl.value;
				engineType = engineType || _tpl.type;
			}
		}

		if (engineType == 'script') {
			deferred.resolve(tpl);
		} else {
			resolveValues([tpl, locals]).spread(function(tpl, locals) {
					engine.render(util.stringify(tpl) + '', locals, engineType, function(err, body) {
						deferred.resolve(body || '');
				 	});
				});
		}
		return deferred.promise;
	}, 'render');
}

function randomIndex(len) {
	return Math.floor(Math.random() * 100000) % len;
}

function random() {
	var args = arguments;
	var len = args.length;

	if (!len) {
		return '';
	}

	return setType(function() {

		return resolveValue(args[len === 1 ? 0 : randomIndex(len)]);
	}, 'random');
}

function join(arr, seperator) {
	if (!Array.isArray(arr)) {
		arr = slice.call(arguments);
		seperator = null;
	}

	return setType(function() {
		var deferred = Q.defer();
		resolveValues(arr).then(function(list) {
			deferred.resolve(list.map(util.stringify).join(seperator || ''));
		});
		return deferred.promise;
	}, 'join');
}

function header(name, value) {
	return setType(function() {
		var result = {};
		if (name != null && value != null) {
			result[name] = String(value);
		}

		return result;
	}, 'headers');
}

function headers(obj) {
	return setType(function() {
		var result = {};
		for (var i in obj) {
			var value = obj[i];
			if (value != null) {
				result[i] = String(value);
			}
		}
		return result;
	}, 'headers');
}

function status(code) {
	return setType(function() {
		return String(code > 0 ? code : 200);
	}, 'status');
}

function json(data) {
	return setType(function() {
		var deferred = Q.defer();
		resolveValue(data).then(function(value) {
			deferred.resolve(util.parseJSON(value) || '');
		});
		return deferred.promise;
	}, 'json');
}

function file(filepath) {

	return setType(function() {
		var deferred = Q.defer();
		if (!filepath || typeof filepath != 'string') {
			deferred.resolve('');
		} else {
			fs.readFile(filepath, function(err, data) {
				deferred.resolve(data || '');
			});
		}
		return deferred.promise;
	}, 'file');
}

function httpGet(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request.get(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'get');
}

function httpPost(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request.post(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'post');
}

function httpRequest(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'request');
}

function removeAcceptEncoding(options) {
	if (!options || !options.headers) {
		return;
	}
	options.headers = util.lowerCaseify(options.headers);
	delete options.headers['accept-encoding'];
}

function merge() {
	var args = arguments;
	if (!args.length) {
		return '';
	}

	return setType(function() {
		var deferred = Q.defer();
		resolveValues(slice.call(args)).then(function(list) {
			deferred.resolve(util.merge.apply(null, list));
		});
		return deferred.promise;
	}, 'merge');
}

function getOuterList(script, req) {
	outerList = [];
	locals = req.locals;
	var context = {
			merge: merge,
			join: join,
			out: out,
			write: out,
			render: render,
			random: random,
			header: header,
			headers: headers,
			status: status,
			statusCode: status,
			json: json,
			file: file,
			get: httpGet,
			post: httpPost,
			request: httpRequest,
			req: {
				method: req.method,
				isHttps: req.isHttps,
				headers: req.headers,
				query: req.query,
				body: req.body,
				locals: req.locals,
				url: req.url
			}
	};

	util.execScriptSync(script, context);
	return outerList;
}

module.exports = function(script, req, callback) {
	var outerList, borted;
	req.on('abort', function() {
		borted = true;
	});

	try {
		outerList = getOuterList(script, req);
	} catch(err) {
		return callback(err);
	}

	var res = new PassThrough();
	var headers = res.headers = {};
	res.statusCode = 200;

	if (!outerList.length) {
		return callback(null, res);
	}

	for (var i = 0, len = outerList.length; i < len; i++) {
		var args = outerList[i];
		var outer = args[0] == null ? '' : args[0];
		args[0] = args[3] ? outer()  : outer;
	}

	var bodyList = [];
	var resDelay = 0;

	outerList.forEach(function(outer) {
		var value = outer[0];
		var delay = outer[1];

		switch(outer[3]) {
			case 'headers':
				extend(headers, util.lowerCaseify(value));
				if (delay > 0) {
					resDelay += delay;
				}
				break;
			case 'status':
				if (value > 0) {
					res.statusCode = value;
				}
				if (delay > 0) {
					resDelay += delay;
				}
				break;
			default:
				bodyList.push(outer);
		}
	});

	if (resDelay > 0) {
		setTimeout(outerHandler, resDelay);
	} else {
		outerHandler();
	}

	function outerHandler() {
		callback(null, res);

		if (borted || !bodyList.length) {
			return res.end();
		}

		Q.all(bodyList.map(function(args) {
			return args[0];
		})).then(function(list) {
			for (var i = 0, len = list.length; i < len; i++) {
				var args = bodyList[i];
				var value = util.stringify(list[i], args[3]);
				bodyList[i] = {
								content: Buffer.isBuffer(value) ? value : new Buffer(value),
								delay: args[1],
								speed: Math.floor(args[2] * 1024 / 8)
							};
			}

			(function write() {
				var body = bodyList.shift();
				if (borted || !body) {
					return res.end();
				}
				var content = body.content;
				var len = content.length;
				if (!len) {
					return write();
				}

				body.delay ? setTimeout(out, body.delay) : out();

				function out() {
					var speed = body.speed;
					if (!speed) {
						res.write(content);
						return write();
					}

					var start = 0;
					(function _write() {
						if (borted || start >= len) {
							return write();
						}
						var end = speed + start;
						setTimeout(function() {
							res.write(content.slice(start, end));
							start = end;
							_write();
						}, ONE_SEC);
					})();
				}
			})();
		});
	}
};<commit_msg>refine code<commit_after>var assert = require('assert');
var Q = require('q');
var fs = require('fs');
var url = require('url');
var request = require('request');
var PassThrough = require('stream').PassThrough;
var extend = require('util')._extend;
var util = require('./util');
var engine = require('./engine');
var dataMgr = require('./data-mgr');
var ONE_SEC = 1000;
var outerList = [];
var slice = Array.prototype.slice;
var TYPE_ATTR = typeof Symbol == 'function' ? Symbol['#vase-type'] : '#vase-type/' + Math.random();
var locals;

function setType(param, type) {
	param[TYPE_ATTR] = type || true;
	return param;
}

function getType(param) {
	return param && param[TYPE_ATTR];
}

function resolveValue(param) {
	if (getType(param)) {
		param = param();
	}

	if (Q.isPromise(param)) {
		return param;
	}

	if (Array.isArray(param)) {
		return Q.all(param);
	}

	var deferred = Q.defer();
	deferred.resolve(param || '');
	return deferred.promise;
}

function resolveValues(params) {

	return Q.all(params.map(function(param) {
		return getType(param) ? param() : (param == null ? '' : param);
	}));
}

function out(data, delay, speed) {
	var delay = delay && parseInt(delay, 10) || 0;
	var speed = speed && parseFloat(speed, 10) || 0;

	outerList.push([data, delay, speed, getType(data)]);
}

function render(tpl, locals, engineType) {

	return setType(function() {
		var deferred = Q.defer();
		if (typeof tpl == 'string' || typeof  tpl == 'number') {
			tpl += '';
			var _tpl = dataMgr.getTpl(tpl);
			if (_tpl) {
				tpl = _tpl.value;
				engineType = engineType || _tpl.type;
			}
		}

		if (engineType == 'script') {
			deferred.resolve(tpl);
		} else {
			resolveValues([tpl, locals]).spread(function(tpl, locals) {
					engine.render(util.stringify(tpl) + '', locals, engineType, function(err, body) {
						deferred.resolve(body || '');
				 	});
				});
		}
		return deferred.promise;
	}, 'render');
}

function randomIndex(len) {
	return Math.floor(Math.random() * 100000) % len;
}

function random() {
	var args = arguments;
	var len = args.length;

	if (!len) {
		return '';
	}

	return setType(function() {

		return resolveValue(args[len === 1 ? 0 : randomIndex(len)]);
	}, 'random');
}

function join(arr, seperator) {
	if (!Array.isArray(arr)) {
		arr = slice.call(arguments);
		seperator = null;
	}

	return setType(function() {
		var deferred = Q.defer();
		resolveValues(arr).then(function(list) {
			deferred.resolve(list.map(util.stringify).join(seperator || ''));
		});
		return deferred.promise;
	}, 'join');
}

function header(name, value) {
	return setType(function() {
		var result = {};
		if (name != null && value != null) {
			result[name] = String(value);
		}

		return result;
	}, 'headers');
}

function headers(obj) {
	return setType(function() {
		var result = {};
		for (var i in obj) {
			var value = obj[i];
			if (value != null) {
				result[i] = String(value);
			}
		}
		return result;
	}, 'headers');
}

function status(code) {
	return setType(function() {
		return String(code > 0 ? code : 200);
	}, 'status');
}

function json(data) {
	return setType(function() {
		var deferred = Q.defer();
		resolveValue(data).then(function(value) {
			deferred.resolve(util.parseJSON(value) || '');
		});
		return deferred.promise;
	}, 'json');
}

function file(filepath) {

	return setType(function() {
		var deferred = Q.defer();
		if (!filepath || typeof filepath != 'string') {
			deferred.resolve('');
		} else {
			fs.readFile(filepath, function(err, data) {
				deferred.resolve(data || '');
			});
		}
		return deferred.promise;
	}, 'file');
}

function httpGet(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request.get(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'get');
}

function httpPost(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request.post(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'post');
}

function httpRequest(options) {
	return setType(function() {
		var deferred = Q.defer();
		removeAcceptEncoding(options);
		request(options, function(err, res, body) {
			deferred.resolve(body || '');
		});
		return deferred.promise;
	}, 'request');
}

function removeAcceptEncoding(options) {
	if (!options || !options.headers) {
		return;
	}
	options.headers = util.lowerCaseify(options.headers);
	delete options.headers['accept-encoding'];
}

function merge() {
	var args = arguments;
	if (!args.length) {
		return '';
	}

	return setType(function() {
		var deferred = Q.defer();
		resolveValues(slice.call(args)).then(function(list) {
			deferred.resolve(util.merge.apply(null, list));
		});
		return deferred.promise;
	}, 'merge');
}

function getOuterList(script, req) {
	outerList = [];
	locals = req.locals;
	var context = {
			merge: merge,
			join: join,
			out: out,
			write: out,
			render: render,
			random: random,
			header: header,
			headers: headers,
			status: status,
			statusCode: status,
			json: json,
			file: file,
			get: httpGet,
			post: httpPost,
			request: httpRequest,
			req: {
				method: req.method,
				isHttps: req.isHttps,
				headers: req.headers,
				query: req.query,
				body: req.body,
				locals: req.locals,
				url: req.url
			}
	};

	util.execScriptSync(script, context);
	return outerList;
}

module.exports = function(script, req, callback) {
	var outerList, borted;
	req.on('abort', function() {
		borted = true;
	});

	try {
		outerList = getOuterList(script, req);
	} catch(err) {
		return callback(err);
	}

	var res = new PassThrough({objectMode: true});
	var headers = res.headers = {};
	res.statusCode = 200;

	if (!outerList.length) {
		return callback(null, res);
	}

	for (var i = 0, len = outerList.length; i < len; i++) {
		var args = outerList[i];
		var outer = args[0] == null ? '' : args[0];
		args[0] = args[3] ? outer()  : outer;
	}

	var bodyList = [];
	var resDelay = 0;

	outerList.forEach(function(outer) {
		var value = outer[0];
		var delay = outer[1];

		switch(outer[3]) {
			case 'headers':
				extend(headers, util.lowerCaseify(value));
				if (delay > 0) {
					resDelay += delay;
				}
				break;
			case 'status':
				if (value > 0) {
					res.statusCode = value;
				}
				if (delay > 0) {
					resDelay += delay;
				}
				break;
			default:
				bodyList.push(outer);
		}
	});

	if (resDelay > 0) {
		setTimeout(outerHandler, resDelay);
	} else {
		outerHandler();
	}

	function outerHandler() {
		callback(null, res);

		if (borted || !bodyList.length) {
			return res.end();
		}

		Q.all(bodyList.map(function(args) {
			return args[0];
		})).then(function(list) {
			for (var i = 0, len = list.length; i < len; i++) {
				var args = bodyList[i];
				var value = util.stringify(list[i], args[3]);
				bodyList[i] = {
								content: Buffer.isBuffer(value) ? value : value + '',
								delay: args[1],
								speed: Math.floor(args[2] * 1024 / 8)
							};
			}

			(function write() {
				var body = bodyList.shift();
				if (borted || !body) {
					return res.end();
				}
				var content = body.content;
				var len = content.length;
				if (!len) {
					return write();
				}

				body.delay ? setTimeout(out, body.delay) : out();

				function out() {
					var speed = body.speed;
					if (!speed) {
						res.write(content);
						return write();
					}

					var start = 0;
					(function _write() {
						if (borted || start >= len) {
							return write();
						}
						var end = speed + start;
						setTimeout(function() {
							res.write(content.slice(start, end));
							start = end;
							_write();
						}, ONE_SEC);
					})();
				}
			})();
		});
	}
};<|endoftext|>


<commit_before>var React        = require('react/addons'),
    triggerEvent = require('trigger-event'),
    scroll       = require('scroll'),
    Beacon       = require('./Beacon'),
    Tooltip      = require('./Tooltip');

var settings = {
    currentIndex: 0,
    initialIndex: 0,
    showTooltip: false,
    steps: [],
    xPos: -1000,
    yPos: -1000
};

var Mixin = {

    getInitialState: function () {
        return {
            joyrideCurrentIndex: 0,
            joyrideLocale: {
                close: 'Close',
                last: 'Last',
                next: 'Next'
            },
            joyrideTooltipOffset: 30,
            joyrideScrollToSteps: true,
            joyrideShowTooltip: false,
            joyrideShowBackdrop: true,
            joyridePlay: false,
            joyrideSteps: [],
            joyrideType: 'guided',
            joyrideCompleteCallback: undefined,
            joyrideStepCallback: undefined,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        };
    },

    componentDidMount: function () {
        var state = this.state;

        this._target = document.createElement('div');
        document.body.appendChild(this._target);

        if (state.joyridePlay && state.joyrideSteps[state.joyrideCurrentIndex]) {
            this._renderLayer();
        }
        window.addEventListener('resize', this._calculatePlacement);
    },

    componentWillUnmount: function () {
        this._unrenderLayer();
        document.body.removeChild(this._target);
        window.removeEventListener('resize', this._calculatePlacement);
    },

    componentDidUpdate: function (prevProps, prevState) {
        var state = this.state,
            opt   = {
                shouldRun: state.joyridePlay && (state.joyrideCurrentIndex !== prevState.joyrideCurrentIndex || state.joyrideSteps !== prevState.joyrideSteps),
                hasStep: Boolean(state.joyrideSteps[state.joyrideCurrentIndex]),
                hasSteps: state.joyrideSteps.length > 0,
                newX: state.joyrideXPos !== prevState.joyrideXPos,
                newY: state.joyrideYPos !== prevState.joyrideYPos,
                toggleTooltip: state.joyrideShowTooltip !== prevState.joyrideShowTooltip
            };

        if ((opt.shouldRun || opt.toggleTooltip || opt.newX || opt.newY) && opt.hasStep) {
            this._renderLayer();
        }
        else if (opt.shouldRun && opt.hasSteps && !opt.hasStep) {
            if (typeof state.joyrideCompleteCallback === 'function') {
                state.joyrideCompleteCallback(state.joyrideSteps);
            }
            this._unrenderLayer();
        }
    },

    /**
     * Starts the tour
     * @param {boolean} [autorun]- Starts with the first tooltip opened
     */
    joyrideStart: function (autorun) {
        autorun = autorun || false;

        this.setState({
            joyrideShowTooltip: autorun,
            joyridePlay: true
        });
    },

    /**
     * Add Steps
     * @param {object|array} steps - Steps to add to the tour
     * @param {boolean} [start] - Starts the tour right away
     */
    joyrideAddSteps: function (steps, start) {
        var tmpSteps  = [],
            realSteps = [];

        start = start || false;

        if (Array.isArray(steps)) {
            steps.forEach(function (s) {
                if (s instanceof Object) {
                    tmpSteps.push(s);
                }
            });
        }
        else {
            tmpSteps = [steps];
        }

        tmpSteps.forEach(function (s) {
            if (document.querySelector(s.selector).offsetParent) {
                realSteps.push(s);
            }
        });

        if (realSteps.length) {
            this.setState(
                React.addons.update(this.state, {
                    joyridePlay: { $set: start },
                    joyrideSteps: { $push: realSteps }
                })
            );
        }
    },

    /**
     * Retrieve the current progress of your tour
     * @returns {{index: (number|*), percentageComplete: number, step: *}}
     */
    joyrideGetProgress: function () {
        return {
            index: this.state.joyrideCurrentIndex,
            percentageComplete: (this.state.joyrideCurrentIndex / this.state.joyrideSteps.length) * 100,
            step: this.state.joyrideSteps[this.state.joyrideCurrentIndex]
        };
    },

    _onClickBeacon: function (e) {
        e.preventDefault();

        this.setState({
            joyrideShowTooltip: true,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        });
    },

    _onClickTooltip: function (e) {
        e.preventDefault();

        var state = this.state,
            type  = e.currentTarget.dataset.type;

        if (typeof state.joyrideStepCallback === 'function') {
            state.joyrideStepCallback(state.joyrideSteps[state.joyrideCurrentIndex]);
        }

        this.setState({
            joyrideShowTooltip: type === 'button' && this.state.joyrideType === 'guided' && Boolean(this.state.joyrideSteps[this.state.joyrideCurrentIndex]),
            joyrideCurrentIndex: state.joyrideCurrentIndex + 1,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        }, function () {
            this._scrollToNextStep();
        }.bind(this));
    },

    _calculatePlacement: function () {
        var state     = this.state,
            step      = state.joyrideSteps[state.joyrideCurrentIndex],
            position,
            body,
            target,
            component,
            placement = {
                x: -1000,
                y: -1000
            };

        if (step) {
            position = step.position ? step.position.toLowerCase() : 'top';
            body = document.body.getBoundingClientRect();
            target = document.querySelector(step.selector).getBoundingClientRect();
            component = document.querySelector(state.joyrideShowTooltip ? '.joyride-tooltip' : '.joyride-beacon').getBoundingClientRect();

            if (window.innerWidth < 768) {
                if (/^left/.test(position)) {
                    position = 'top';
                }
                else if (/^right/.test(position)) {
                    position = 'bottom';
                }
            }

            // Calculate x position
            if (/^left/.test(position)) {
                placement.x = target.left - (state.joyrideShowTooltip ? component.width + state.joyrideTooltipOffset : component.width / 2);
            }
            else if (/^right/.test(position)) {
                placement.x = target.left + target.width - (state.joyrideShowTooltip ? -state.joyrideTooltipOffset : component.width / 2);
            }
            else {
                placement.x = target.left + target.width / 2 - component.width / 2;
            }

            // Calculate y position
            if (/^top/.test(position)) {
                placement.y = (target.top - body.top) - (state.joyrideShowTooltip ? component.height + state.joyrideTooltipOffset : component.height / 2);
            }
            else if (/^bottom/.test(position)) {
                placement.y = (target.top - body.top) + target.height - (state.joyrideShowTooltip ? -state.joyrideTooltipOffset : component.height / 2);
            }
            else {
                placement.y = (target.top - body.top) + target.height / 2 - component.height / 2 + (state.joyrideShowTooltip ? state.joyrideTooltipOffset : 0);
            }

            if (/^bottom|^top/.test(position)) {
                if (/left/.test(position)) {
                    placement.x = target.left - (state.joyrideShowTooltip ? state.joyrideTooltipOffset : component.width / 2);
                }
                else if (/right/.test(position)) {
                    placement.x = target.left + target.width - (state.joyrideShowTooltip ? component.width - state.joyrideTooltipOffset : component.width / 2);
                }
            }

            this.setState({
                joyrideXPos: this._preventWindowOverflow(Math.ceil(placement.x), 'x', component.width, component.height),
                joyrideYPos: this._preventWindowOverflow(Math.ceil(placement.y), 'y', component.width, component.height)
            });
        }
    },

    _preventWindowOverflow: function (value, axis, elWidth, elHeight) {
        var winWidth = window.innerWidth;
        var docHeight = document.body.offsetHeight;

        if (axis.toLowerCase() === 'x') {
            if (value + elWidth > winWidth) {
                value = winWidth - elWidth - 10;
            }
            else if (value < 0) {
                value = 10;
            }
        }
        else if (axis.toLowerCase() === 'y') {
            if (value + elHeight > docHeight) {
                value = docHeight - elHeight - 10;
            }
            else if (value < 0) {
                value = 10;
            }
        }

        return value;
    },

    _scrollToNextStep: function (cb) {
        if (this.state.joyrideSteps[this.state.joyrideCurrentIndex] && this.state.joyrideScrollToSteps) {
            scroll.top(document.body, this.state.joyrideYPos - window.innerHeight / 2, cb);
        }
    },

    _renderLayer: function () {
        React.render(this._renderCurrentStep(), this._target);
        if (this.state.joyrideXPos < 0) {
            this._calculatePlacement();
        }
    },

    _unrenderLayer: function () {
        React.unmountComponentAtNode(this._target);
    },

    _renderCurrentStep: function () {
        var state       = this.state,
            component,
            currentStep = state.joyrideSteps[state.joyrideCurrentIndex],
            hasNextStep = Boolean(state.joyrideSteps[state.joyrideCurrentIndex + 1]),
            buttonText  = state.joyrideType === 'guided' ? (hasNextStep ? (state.joyrideLocale.next + ' ' + (state.joyrideCurrentIndex + 1) + '/' + state.joyrideSteps.length) : state.joyrideLocale.last) : state.joyrideLocale.close,
            target      = currentStep && currentStep.selector ? document.querySelector(currentStep.selector) : null,
            cssPosition = target ? target.style.position : null;

        if (target) {
            if (state.joyrideShowTooltip) {
                component = React.createElement(Tooltip, {
                    animate: state.joyrideXPos > -1,
                    cssPosition: cssPosition,
                    xPos: state.joyrideXPos,
                    yPos: state.joyrideYPos,
                    step: currentStep,
                    buttonText: buttonText,
                    showBackdrop: state.joyrideShowBackdrop,
                    onClick: this._onClickTooltip
                });
            }
            else {
                component = React.createElement(Beacon, {
                    cssPosition: cssPosition,
                    xPos: state.joyrideXPos,
                    yPos: state.joyrideYPos,
                    handleClick: this._onClickBeacon
                });
            }
        }

        return component;
    }
};

module.exports = Mixin;
<commit_msg>fix autorun<commit_after>var React        = require('react/addons'),
    triggerEvent = require('trigger-event'),
    scroll       = require('scroll'),
    Beacon       = require('./Beacon'),
    Tooltip      = require('./Tooltip');

var settings = {
    currentIndex: 0,
    initialIndex: 0,
    showTooltip: false,
    steps: [],
    xPos: -1000,
    yPos: -1000
};

var Mixin = {

    getInitialState: function () {
        return {
            joyrideCurrentIndex: 0,
            joyrideLocale: {
                close: 'Close',
                last: 'Last',
                next: 'Next'
            },
            joyrideTooltipOffset: 30,
            joyrideScrollToSteps: true,
            joyrideShowTooltip: false,
            joyrideShowBackdrop: true,
            joyridePlay: false,
            joyrideSteps: [],
            joyrideType: 'guided',
            joyrideCompleteCallback: undefined,
            joyrideStepCallback: undefined,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        };
    },

    componentDidMount: function () {
        var state = this.state;

        this._target = document.createElement('div');
        document.body.appendChild(this._target);

        if (state.joyridePlay && state.joyrideSteps[state.joyrideCurrentIndex]) {
            this._renderLayer();
        }
        window.addEventListener('resize', this._calculatePlacement);
    },

    componentWillUnmount: function () {
        this._unrenderLayer();
        document.body.removeChild(this._target);
        window.removeEventListener('resize', this._calculatePlacement);
    },

    componentDidUpdate: function (prevProps, prevState) {
        var state = this.state,
            opt   = {
                shouldRun: state.joyridePlay && (state.joyridePlay !== prevState.joyridePlay || state.joyrideCurrentIndex !== prevState.joyrideCurrentIndex || state.joyrideSteps !== prevState.joyrideSteps),
                hasStep: Boolean(state.joyrideSteps[state.joyrideCurrentIndex]),
                hasSteps: state.joyrideSteps.length > 0,
                newX: state.joyrideXPos !== prevState.joyrideXPos,
                newY: state.joyrideYPos !== prevState.joyrideYPos,
                toggleTooltip: state.joyrideShowTooltip !== prevState.joyrideShowTooltip
            };

        if ((opt.shouldRun || opt.toggleTooltip || opt.newX || opt.newY) && opt.hasStep) {
            this._renderLayer();
        }
        else if (opt.shouldRun && opt.hasSteps && !opt.hasStep) {
            if (typeof state.joyrideCompleteCallback === 'function') {
                state.joyrideCompleteCallback(state.joyrideSteps);
            }
            this._unrenderLayer();
        }
    },

    /**
     * Starts the tour
     * @param {boolean} [autorun]- Starts with the first tooltip opened
     */
    joyrideStart: function (autorun) {
        autorun = autorun || false;

        this.setState({
            joyrideShowTooltip: autorun,
            joyridePlay: true
        });
    },

    /**
     * Add Steps
     * @param {object|array} steps - Steps to add to the tour
     * @param {boolean} [start] - Starts the tour right away
     */
    joyrideAddSteps: function (steps, start) {
        var tmpSteps  = [],
            realSteps = [];

        start = start || false;

        if (Array.isArray(steps)) {
            steps.forEach(function (s) {
                if (s instanceof Object) {
                    tmpSteps.push(s);
                }
            });
        }
        else {
            tmpSteps = [steps];
        }

        tmpSteps.forEach(function (s) {
            if (document.querySelector(s.selector).offsetParent) {
                realSteps.push(s);
            }
        });

        if (realSteps.length) {
            this.setState(
                React.addons.update(this.state, {
                    joyridePlay: { $set: start },
                    joyrideSteps: { $push: realSteps }
                })
            );
        }
    },

    /**
     * Retrieve the current progress of your tour
     * @returns {{index: (number|*), percentageComplete: number, step: *}}
     */
    joyrideGetProgress: function () {
        return {
            index: this.state.joyrideCurrentIndex,
            percentageComplete: (this.state.joyrideCurrentIndex / this.state.joyrideSteps.length) * 100,
            step: this.state.joyrideSteps[this.state.joyrideCurrentIndex]
        };
    },

    _onClickBeacon: function (e) {
        e.preventDefault();

        this.setState({
            joyrideShowTooltip: true,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        });
    },

    _onClickTooltip: function (e) {
        e.preventDefault();

        var state = this.state,
            type  = e.currentTarget.dataset.type;

        if (typeof state.joyrideStepCallback === 'function') {
            state.joyrideStepCallback(state.joyrideSteps[state.joyrideCurrentIndex]);
        }

        this.setState({
            joyrideShowTooltip: type === 'button' && this.state.joyrideType === 'guided' && Boolean(this.state.joyrideSteps[this.state.joyrideCurrentIndex]),
            joyrideCurrentIndex: state.joyrideCurrentIndex + 1,
            joyrideXPos: -1000,
            joyrideYPos: -1000
        }, function () {
            this._scrollToNextStep();
        }.bind(this));
    },

    _calculatePlacement: function () {
        var state     = this.state,
            step      = state.joyrideSteps[state.joyrideCurrentIndex],
            position,
            body,
            target,
            component = document.querySelector((state.joyrideShowTooltip ? '.joyride-tooltip' : '.joyride-beacon')),
            placement = {
                x: -1000,
                y: -1000
            };

        if (step && (!component.classList.contains('animate') || state.joyrideXPos < 0)) {
            position = step.position ? step.position.toLowerCase() : 'top';
            body = document.body.getBoundingClientRect();
            target = document.querySelector(step.selector).getBoundingClientRect();
            component = component.getBoundingClientRect();

            if (window.innerWidth < 768) {
                if (/^left/.test(position)) {
                    position = 'top';
                }
                else if (/^right/.test(position)) {
                    position = 'bottom';
                }
            }

            // Calculate x position
            if (/^left/.test(position)) {
                placement.x = target.left - (state.joyrideShowTooltip ? component.width + state.joyrideTooltipOffset : component.width / 2);
            }
            else if (/^right/.test(position)) {
                placement.x = target.left + target.width - (state.joyrideShowTooltip ? -state.joyrideTooltipOffset : component.width / 2);
            }
            else {
                placement.x = target.left + target.width / 2 - component.width / 2;
            }

            // Calculate y position
            if (/^top/.test(position)) {
                placement.y = (target.top - body.top) - (state.joyrideShowTooltip ? component.height + state.joyrideTooltipOffset : component.height / 2);
            }
            else if (/^bottom/.test(position)) {
                placement.y = (target.top - body.top) + target.height - (state.joyrideShowTooltip ? -state.joyrideTooltipOffset : component.height / 2);
            }
            else {
                placement.y = (target.top - body.top) + target.height / 2 - component.height / 2 + (state.joyrideShowTooltip ? state.joyrideTooltipOffset : 0);
            }

            if (/^bottom|^top/.test(position)) {
                if (/left/.test(position)) {
                    placement.x = target.left - (state.joyrideShowTooltip ? state.joyrideTooltipOffset : component.width / 2);
                }
                else if (/right/.test(position)) {
                    placement.x = target.left + target.width - (state.joyrideShowTooltip ? component.width - state.joyrideTooltipOffset : component.width / 2);
                }
            }

            this.setState({
                joyrideXPos: this._preventWindowOverflow(Math.ceil(placement.x), 'x', component.width, component.height),
                joyrideYPos: this._preventWindowOverflow(Math.ceil(placement.y), 'y', component.width, component.height)
            });
        }
    },

    _preventWindowOverflow: function (value, axis, elWidth, elHeight) {
        var winWidth = window.innerWidth;
        var docHeight = document.body.offsetHeight;

        if (axis.toLowerCase() === 'x') {
            if (value + elWidth > winWidth) {
                value = winWidth - elWidth - 10;
            }
            else if (value < 0) {
                value = 10;
            }
        }
        else if (axis.toLowerCase() === 'y') {
            if (value + elHeight > docHeight) {
                value = docHeight - elHeight - 10;
            }
            else if (value < 0) {
                value = 10;
            }
        }

        return value;
    },

    _scrollToNextStep: function (cb) {
        if (this.state.joyrideSteps[this.state.joyrideCurrentIndex] && this.state.joyrideScrollToSteps) {
            scroll.top(document.body, this.state.joyrideYPos - window.innerHeight / 2, cb);
        }
    },

    _renderLayer: function () {
        React.render(this._renderCurrentStep(), this._target);

        this._calculatePlacement();
    },

    _unrenderLayer: function () {
        React.unmountComponentAtNode(this._target);
    },

    _renderCurrentStep: function () {
        var state       = this.state,
            component,
            currentStep = state.joyrideSteps[state.joyrideCurrentIndex],
            hasNextStep = Boolean(state.joyrideSteps[state.joyrideCurrentIndex + 1]),
            buttonText  = state.joyrideType === 'guided' ? (hasNextStep ? (state.joyrideLocale.next + ' ' + (state.joyrideCurrentIndex + 1) + '/' + state.joyrideSteps.length) : state.joyrideLocale.last) : state.joyrideLocale.close,
            target      = currentStep && currentStep.selector ? document.querySelector(currentStep.selector) : null,
            cssPosition = target ? target.style.position : null;

        if (target) {
            if (state.joyrideShowTooltip) {
                component = React.createElement(Tooltip, {
                    animate: state.joyrideXPos > -1,
                    cssPosition: cssPosition,
                    xPos: state.joyrideXPos,
                    yPos: state.joyrideYPos,
                    step: currentStep,
                    buttonText: buttonText,
                    showBackdrop: state.joyrideShowBackdrop,
                    onClick: this._onClickTooltip
                });
            }
            else {
                component = React.createElement(Beacon, {
                    cssPosition: cssPosition,
                    xPos: state.joyrideXPos,
                    yPos: state.joyrideYPos,
                    handleClick: this._onClickBeacon
                });
            }
        }

        return component;
    }
};

module.exports = Mixin;
<|endoftext|>


<commit_before>var Emitter = require( 'events' ).EventEmitter
var inherit = require( 'bloodline' )
var log = require( 'debug' )( 'ZEUGBERG:PAGE' )

/**
 * Page Constructor
 * @param {nw.gui.Window} chrome
 * @return {Page}
 */
function Page( chrome ) {

  if( !(this instanceof Page) )
    return new Page( chrome )

  // Inherit from EventEmitter
  Emitter.call( this )

  this.chrome = chrome
  this.chrome.on( 'attachEvents',
    this.attachEvents.bind( this )
  )

  this.chrome.on( 'loaded', this.updateGlobals.bind( this ) )

  this.checklist = [
    'loaded',
    // 'inspector:attached',
    // 'inspector:network',
  ]

}

/**
 * Globals to map over into the
 * global context before running a test
 * @type {Array}
 */
Page.globals = [
  'window',
  'document',
  'setTimeout',
  'setInterval',
  'setImmediate',
  'clearTimeout',
  'clearInterval',
  'clearImmediate',
  'Date',
  'XMLHttpRequest',
]

/**
 * Page prototype
 * @type {Object}
 */
Page.prototype = {

  /**
   * Page Constructor
   * @type {Function}
   */
  constructor: Page,

  /**
   * [updateGlobals description]
   */
  updateGlobals: function() {

    log( 'UPDATE_GLOBALS' )

    var frame = this.chrome.frame

    // Map over the testing window's globals to node's context
    // to avoid tests accessing wrong contexts
    // (also, workaround for V8's auto context switching)
    Page.globals.forEach( function( varname ) {
      global[ varname ] = frame.eval( null, 'this.' + varname )
    })

    // DOM helpers
    // global.$ = context.$
    // global.$$ = context.$$
    global.$ = function $( selector, startNode ) {
      return ( startNode || global.document )
        .querySelector( selector )
    }

    global.$$ = function $$( selector, startNode ) {
      return ( startNode || global.document )
        .querySelectorAll( selector )
    }

    // Page control helper
    global.page = this

    if( this.checklist.length === 0 )
      this.emit( 'ready' )

  },

  /**
   * Attach events to window frame
   * @return {Undefined}
   */
  attachEvents: function() {

    var self = this
    var checklist = this.checklist

    function wait( event ) {
      return function check() {
        var index = checklist.indexOf( event )
        if( ~index )
          checklist.splice( index, 1 )
        if( checklist.length === 0 ) {
          log( 'READY' )
          self.emit( 'ready' )
        }
      }
    }

    for( var i in this.checklist ) {
      var event = this.checklist[i]
      this.chrome.once( event, wait( event ) )
    }

  },

  /**
   * Reload the page
   * @param  {Function} callback()
   * @return {Page}
   */
  reload: function( callback ) {

    if( typeof callback === 'function' )
      this.once( 'ready', callback )

    this.chrome.reload()
    return this

  },

  /**
   * Navigate to a given location
   * @param  {String}   path
   * @param  {Function} callback( error, location )
   * @return {Page}
   */
  navigate: function( path, callback ) {

    if( typeof callback === 'function' )
      this.once( 'ready', callback )

    // TODO:
    // - [ ] Pass HTTP status to 'ready' event
    //       (Needs Debugger Protocol support)

    this.chrome.location = path
    return this

  },

  resize: function( width, height, callback ) {

    var self = this
    var done = callback.bind( this )

    this.chrome.once( 'resized', done )
    this.chrome.frame.resizeTo( width, height )

    return this

  },

  /**
   * Capture the rendered page
   * @param  {Function} callback( error, buffer )
   * @return {Page}
   */
  capture: function( callback ) {

    var self = this
    var done = callback.bind( this )
    var options = { format: 'png', datatype: 'buffer' }

    window.requestAnimationFrame( function() {
      try {
        self.chrome.frame.capturePage( function( data ) {
          done( null, data )
        }, options )
      } catch( error ) {
        done( error )
      }
    })

    return this

  },

}

// Inherit from EventEmitter
inherit( Page, Emitter )
// Exports
module.exports = Page
<commit_msg>Updated lib/page: Simplified page.capture()<commit_after>var Emitter = require( 'events' ).EventEmitter
var inherit = require( 'bloodline' )
var log = require( 'debug' )( 'ZEUGBERG:PAGE' )

/**
 * Page Constructor
 * @param {nw.gui.Window} chrome
 * @return {Page}
 */
function Page( chrome ) {

  if( !(this instanceof Page) )
    return new Page( chrome )

  // Inherit from EventEmitter
  Emitter.call( this )

  this.chrome = chrome
  this.chrome.on( 'attachEvents',
    this.attachEvents.bind( this )
  )

  this.chrome.on( 'loaded', this.updateGlobals.bind( this ) )

  this.checklist = [
    'loaded',
    // 'inspector:attached',
    // 'inspector:network',
  ]

}

/**
 * Globals to map over into the
 * global context before running a test
 * @type {Array}
 */
Page.globals = [
  'window',
  'document',
  'setTimeout',
  'setInterval',
  'setImmediate',
  'clearTimeout',
  'clearInterval',
  'clearImmediate',
  'Date',
  'XMLHttpRequest',
]

/**
 * Page prototype
 * @type {Object}
 */
Page.prototype = {

  /**
   * Page Constructor
   * @type {Function}
   */
  constructor: Page,

  /**
   * [updateGlobals description]
   */
  updateGlobals: function() {

    log( 'UPDATE_GLOBALS' )

    var frame = this.chrome.frame

    // Map over the testing window's globals to node's context
    // to avoid tests accessing wrong contexts
    // (also, workaround for V8's auto context switching)
    Page.globals.forEach( function( varname ) {
      global[ varname ] = frame.eval( null, 'this.' + varname )
    })

    // DOM helpers
    // global.$ = context.$
    // global.$$ = context.$$
    global.$ = function $( selector, startNode ) {
      return ( startNode || global.document )
        .querySelector( selector )
    }

    global.$$ = function $$( selector, startNode ) {
      return ( startNode || global.document )
        .querySelectorAll( selector )
    }

    // Page control helper
    global.page = this

    if( this.checklist.length === 0 )
      this.emit( 'ready' )

  },

  /**
   * Attach events to window frame
   * @return {Undefined}
   */
  attachEvents: function() {

    var self = this
    var checklist = this.checklist

    function wait( event ) {
      return function check() {
        var index = checklist.indexOf( event )
        if( ~index )
          checklist.splice( index, 1 )
        if( checklist.length === 0 ) {
          log( 'READY' )
          self.emit( 'ready' )
        }
      }
    }

    for( var i in this.checklist ) {
      var event = this.checklist[i]
      this.chrome.once( event, wait( event ) )
    }

  },

  /**
   * Reload the page
   * @param  {Function} callback()
   * @return {Page}
   */
  reload: function( callback ) {

    if( typeof callback === 'function' )
      this.once( 'ready', callback )

    this.chrome.reload()
    return this

  },

  /**
   * Navigate to a given location
   * @param  {String}   path
   * @param  {Function} callback( error, location )
   * @return {Page}
   */
  navigate: function( path, callback ) {

    if( typeof callback === 'function' )
      this.once( 'ready', callback )

    // TODO:
    // - [ ] Pass HTTP status to 'ready' event
    //       (Needs Debugger Protocol support)

    this.chrome.location = path
    return this

  },

  resize: function( width, height, callback ) {

    var self = this
    var done = callback.bind( this )

    this.chrome.once( 'resized', done )
    this.chrome.frame.resizeTo( width, height )

    return this

  },

  /**
   * Capture the rendered page
   * @param  {Function} callback( error, buffer )
   * @return {Page}
   */
  capture: function( width, callback ) {

    var self = this

    if( typeof width === 'function' ) {
      callback = width
      width = 1440
    }

    var done = callback.bind( this )
    var options = { format: 'png', datatype: 'buffer' }

    // Save current dimensions,
    // to restore them afterwards
    var prevWidth = this.chrome.frame.width
    var prevHeight = this.chrome.frame.height

    // TODO: Determine end of painting through the inspector
    // For now, nextTick(), just to be sure...
    process.nextTick( function() {
      // Wait for an animationFrame to make sure
      // the renderer has painted at least once
      window.requestAnimationFrame( function() {
        // Determine the full body height
        var height = document.body.scrollHeight
        // And resize to it, to capture the full page,
        // not just the viewport
        self.resize( width, height, function() {
          try {
            // Capture the page content
            self.chrome.frame.capturePage( function( buffer ) {
              // Restore previous dimensions
              self.resize( prevWidth, prevHeight, function() {
                done( null, buffer )
              })
            }, options )
          } catch( error ) {
            done( error )
          }
        })
      })
    })

    return this

  },

}

// Inherit from EventEmitter
inherit( Page, Emitter )
// Exports
module.exports = Page
<|endoftext|>


<commit_before>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright (c) 2014, Joyent, Inc.
 */

/*
 * Client library for the SDC Packages API (PAPI)
 */

var util = require('util');
var format = util.format;
var restify = require('restify');
var qs = require('querystring');
var assertions = require('./assertions');

// --- Globals

var assertFunction = assertions.assertFunction;
var assertNumber = assertions.assertNumber;
var assertObject = assertions.assertObject;
var assertString = assertions.assertString;

var ResourceNotFoundError = restify.ResourceNotFoundError;

// Note this is not a constructor!.
function PAPI(options) {
    if (typeof (options) !== 'object') {
        throw new TypeError('options (Object) required');
    }

    if (typeof (options.url) !== 'string') {
        throw new TypeError('options.url (String) required');
    }

    if (!options['X-Api-Version']) {
        options['X-Api-Version'] = '~7.0';
    }

    var client = restify.createJsonClient(options);

    /**
     * Adds a new package to PAPI
     *
     * See https://mo.joyent.com/docs/papi/master/#packageobjects for the
     * details on expected attributes
     *
     * @param {Object} pkg the entry to add.
     * @param {Function} cb of the form fn(err, pkg).
     * @throws {TypeError} on bad input.
     */
    function add(pkg, cb) {
        assertObject('pkg', pkg);
        assertFunction('cb', cb);

        return client.post('/packages', pkg, function (err, req, res, pkg) {
            if (err) {
                return cb(err);
            }
            return cb(null, pkg);
        });
    }

    /**
     * Looks up a package by uuid.
     *
     * @param {String} uuid for a package.
     * @param {Object} options params passed to PAPI
     * @param {Function} cb of the form f(err, pkg).
     * @throws {TypeError} on bad input.
     */
    function get(uuid, options, cb) {
        assertString('uuid', uuid);
        assertObject('options', options);
        assertFunction('cb', cb);

        var path = createPath('/packages/' + uuid, options);

        return client.get(path, function (err, req, res, pkg) {
            if (err) {
                return cb(err);
            }

            return cb(null, pkg);
        });
    }

    /**
     * Deletes a pkg record.
     *
     * @param {String} uuid the uuid of the record you received from get().
     * @param {Object} opt the uuid of the record you received from get().
     * @param {Function} cb of the form fn(err).
     * @throws {TypeError} on bad input.
     */
    function del(uuid, options, cb) {
        assertString('uuid', uuid);
        assertObject('options', options);
        assertFunction('cb', cb);

        var path = createPath('/packages/' + uuid, options);

        return client.del(path, cb);
    }

    /**
     * Updates a package record.
     *
     * Note you don't need to pass a whole copy of the pkg to changes, just the
     * attributes you want to modify
     *
     * @param {Object} pkg the package record you got from get.
     * @param {Object} changes the pkg to *replace* original package with
     * @param {Function} cb of the form fn(err).
     * @throws {TypeError} on bad input.
     */
    function update(uuid, changes, cb) {
        assertString('uuid', uuid);
        assertObject('changes', changes);
        assertFunction('cb', cb);

        var p = '/packages/' + uuid;
        return client.put(p, changes, function (err, req, res, pack) {
            if (err) {
                return cb(err);
            }

            return cb(null, pack);
        });
    }

    /**
     * Loads a list of packages.
     *
     * If the filter is a string, it will be fed as an LDIF filter directly to
     * PAPI. If it is a hash, each k/v pair will be passed to PAPI as
     * constraints on the query.
     *
     * See https://mo.joyent.com/docs/papi/master/#ListPackages for detailed
     * information regarding search filter and pagination options accepted
     *
     * The count argument retrieved on success will provide the total number
     * of packages matching the given search filter (retrieved by PAPI as
     * x-resource-count HTTP header).
     *
     * When passing a filter object (not a string), the query arguments will be
     * escaped according to ldif filter rules. This can be overridden with an
     * option, but don't do so unless you're 100% confident the query args
     * aren't potentially tainted.
     *
     * @param {String or Object} provided LDAP filter.
     * @param {Object} pagination options when desired.
     * @param {Function} callback cb of the form fn(err, pkgs, count).
     * @throws {TypeError} on bad input.
     */
    function list(filter, options, cb) {
        assertObject('options', options);
        assertFunction('cb', cb);

        var escape = options.escape;
        delete options.escape;

        var q = [];

        if (typeof (filter) === 'string') {
            q.push('filter=' + escapeParam(filter, false));
        } else {
            Object.keys(filter).forEach(function (k) {
                q.push(k + '=' + escapeParam(filter[k], escape));
            });
        }

        Object.keys(options).forEach(function (k) {
            q.push(k + '=' + options[k]);
        });

        var p = '/packages';

        if (q.length) {
            p = p + '?' + q.join('&');
        }

        return client.get(p, function (err, req, res, pkgs) {
            if (err) {
                return cb(err);
            }

            var count = Number(res.headers['x-resource-count']);
            return cb(null, pkgs, count);
        });
    }

   /**
    * Escapes param data being sent to PAPI.
    *
    * PAPI accepts special characters used for LDIF filters in its params
    * when making queries. This is useful for ops, but undesirable for
    * most applications (and especially data that may carry taint from
    * outside). This function escapes data (both ldif and query forms) so
    * that they're safe to use as params passed to PAPI.
    *
    * @param data the data to escape
    * @param escape whether to escape the data for ldif
    */
    function escapeParam(data, escape) {
        if (typeof (data) !== 'string')
            return data;

        // treat undefined as true as well
        if (escape !== false) {
            data = data.replace('(',  '{\\28}').
                        replace(')',  '{\\29}').
                        replace('\\', '{\\5c}').
                        replace('*',  '{\\2a}').
                        replace('/',  '{\\2f}');
        }

        return qs.escape(data);
    }

   /**
    * Append params to path.
    *
    * @param {String} path the path without params
    * @param {Object} options the args to apply to the end of the path
    */
    function createPath(path, options) {
        assertString('path', path);
        assertObject('options', options);

        var escape = options.escape;
        delete options.escape;

        var q = [];

        Object.keys(options).forEach(function (k) {
            q.push(k + '=' + escapeParam(options[k], escape));
        });

        if (q.length)
            path += '?' + q.join('&');

        return path;
    }

    return {
        add: add,
        get: get,
        list: list,
        del: del,
        update: update,
        client: client
    };
}

module.exports = PAPI;
<commit_msg>PAPI-113 papi client should allow passing of headers.<commit_after>/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
 * Copyright (c) 2014, Joyent, Inc.
 */

/*
 * Client library for the SDC Packages API (PAPI)
 */

var util = require('util');
var format = util.format;
var restify = require('restify');
var qs = require('querystring');
var assertions = require('./assertions');

// --- Globals

var assertFunction = assertions.assertFunction;
var assertNumber = assertions.assertNumber;
var assertObject = assertions.assertObject;
var assertString = assertions.assertString;

var ResourceNotFoundError = restify.ResourceNotFoundError;

// Note this is not a constructor!.
function PAPI(options) {
    if (typeof (options) !== 'object') {
        throw new TypeError('options (Object) required');
    }

    if (typeof (options.url) !== 'string') {
        throw new TypeError('options.url (String) required');
    }

    if (!options['X-Api-Version']) {
        options['X-Api-Version'] = '~7.0';
    }

    var client = restify.createJsonClient(options);

    /**
     * Adds a new package to PAPI
     *
     * See https://mo.joyent.com/docs/papi/master/#packageobjects for the
     * details on expected attributes
     *
     * @param {Object} pkg the entry to add.
     * @param {Object} request options.
     * @param {Function} cb of the form fn(err, pkg).
     * @throws {TypeError} on bad input.
     */
    function add(pkg, options, cb) {

        if (typeof (options) === 'function') {
            cb = options;
            options = {};
        }

        assertObject('pkg', pkg);
        assertFunction('cb', cb);

        var opts = {path: '/packages'};
        if (options.headers) {
            opts.headers = options.headers;
        }

        return client.post(opts, pkg, function (err, req, res, pkg) {
            if (err) {
                return cb(err);
            }
            return cb(null, pkg);
        });
    }

    /**
     * Looks up a package by uuid.
     *
     * @param {String} uuid for a package.
     * @param {Object} options params passed to PAPI
     * @param {Function} cb of the form f(err, pkg).
     * @throws {TypeError} on bad input.
     */
    function get(uuid, options, cb) {
        assertString('uuid', uuid);
        assertObject('options', options);
        assertFunction('cb', cb);

        var path = createPath('/packages/' + uuid, options);
        var opts = {path: path};
        if (options.headers) {
            opts.headers = options.headers;
        }

        return client.get(opts, function (err, req, res, pkg) {
            if (err) {
                return cb(err);
            }

            return cb(null, pkg);
        });
    }

    /**
     * Deletes a pkg record.
     *
     * @param {String} uuid the uuid of the record you received from get().
     * @param {Object} opt the uuid of the record you received from get().
     * @param {Function} cb of the form fn(err).
     * @throws {TypeError} on bad input.
     */
    function del(uuid, options, cb) {
        assertString('uuid', uuid);
        assertObject('options', options);
        assertFunction('cb', cb);

        var path = createPath('/packages/' + uuid, options);
        var opts = {path: path};
        if (options.headers) {
            opts.headers = options.headers;
        }

        return client.del(opts, cb);
    }

    /**
     * Updates a package record.
     *
     * Note you don't need to pass a whole copy of the pkg to changes, just the
     * attributes you want to modify
     *
     * @param {Object} pkg the package record you got from get.
     * @param {Object} changes the pkg to *replace* original package with
     * @param {Object} request options.
     * @param {Function} cb of the form fn(err).
     * @throws {TypeError} on bad input.
     */
    function update(uuid, changes, options, cb) {

        if (typeof (options) === 'function') {
            cb = options;
            options = {};
        }

        assertString('uuid', uuid);
        assertObject('changes', changes);
        assertFunction('cb', cb);

        var p = '/packages/' + uuid;
        var opts = {path: p};
        if (options.headers) {
            opts.headers = options.headers;
        }
        return client.put(opts, changes, function (err, req, res, pack) {
            if (err) {
                return cb(err);
            }

            return cb(null, pack);
        });
    }

    /**
     * Loads a list of packages.
     *
     * If the filter is a string, it will be fed as an LDIF filter directly to
     * PAPI. If it is a hash, each k/v pair will be passed to PAPI as
     * constraints on the query.
     *
     * See https://mo.joyent.com/docs/papi/master/#ListPackages for detailed
     * information regarding search filter and pagination options accepted
     *
     * The count argument retrieved on success will provide the total number
     * of packages matching the given search filter (retrieved by PAPI as
     * x-resource-count HTTP header).
     *
     * When passing a filter object (not a string), the query arguments will be
     * escaped according to ldif filter rules. This can be overridden with an
     * option, but don't do so unless you're 100% confident the query args
     * aren't potentially tainted.
     *
     * @param {String or Object} provided LDAP filter.
     * @param {Object} pagination options when desired.
     * @param {Function} callback cb of the form fn(err, pkgs, count).
     * @throws {TypeError} on bad input.
     */
    function list(filter, options, cb) {
        assertObject('options', options);
        assertFunction('cb', cb);

        var escape = options.escape;
        delete options.escape;
        var headers = options.headers;
        delete options.headers;

        var q = [];

        if (typeof (filter) === 'string') {
            q.push('filter=' + escapeParam(filter, false));
        } else {
            Object.keys(filter).forEach(function (k) {
                q.push(k + '=' + escapeParam(filter[k], escape));
            });
        }

        Object.keys(options).forEach(function (k) {
            q.push(k + '=' + options[k]);
        });

        var p = '/packages';

        // XXX should use opts.query instead
        if (q.length) {
            p = p + '?' + q.join('&');
        }

        var opts = {path: p};
        if (headers) {
            opts.headers = headers;
        }

        return client.get(opts, function (err, req, res, pkgs) {
            if (err) {
                return cb(err);
            }

            var count = Number(res.headers['x-resource-count']);
            return cb(null, pkgs, count);
        });
    }

   /**
    * Escapes param data being sent to PAPI.
    *
    * PAPI accepts special characters used for LDIF filters in its params
    * when making queries. This is useful for ops, but undesirable for
    * most applications (and especially data that may carry taint from
    * outside). This function escapes data (both ldif and query forms) so
    * that they're safe to use as params passed to PAPI.
    *
    * @param data the data to escape
    * @param escape whether to escape the data for ldif
    */
    function escapeParam(data, escape) {
        if (typeof (data) !== 'string')
            return data;

        // treat undefined as true as well
        if (escape !== false) {
            data = data.replace('(',  '{\\28}').
                        replace(')',  '{\\29}').
                        replace('\\', '{\\5c}').
                        replace('*',  '{\\2a}').
                        replace('/',  '{\\2f}');
        }

        return qs.escape(data);
    }

   /**
    * Append params to path.
    *
    * @param {String} path the path without params
    * @param {Object} options the args to apply to the end of the path
    */
    function createPath(path, options) {
        assertString('path', path);
        assertObject('options', options);

        var escape = options.escape;
        delete options.escape;

        var q = [];

        Object.keys(options).forEach(function (k) {
            q.push(k + '=' + escapeParam(options[k], escape));
        });

        if (q.length)
            path += '?' + q.join('&');

        return path;
    }

    return {
        add: add,
        get: get,
        list: list,
        del: del,
        update: update,
        client: client
    };
}

module.exports = PAPI;
<|endoftext|>


<commit_before>var cp  = require('child_process');
var p   = require('os').platform().toLowerCase();

function probe(addr, config, callback) {
    config = config || {};

    var ls  = null;
    var log = config.log || console.log;
    var outstring = '';

    config = {
        numeric:  config.numeric  === undefined ? true : config.numeric,
        timeout:  parseInt(config.timeout  === undefined ? 2    : config.timeout, 10),
        minReply: parseInt(config.minReply === undefined ? 1    : config.minReply, 10),
        extra:    config.extra || []
    };

    var args    = [];
    var xfamily = ['linux', 'sunos', 'unix'];
    var regex   = /=.*[<|=]([0-9]*).*TTL|ttl..*=([0-9\.]*)/;

    if (xfamily.indexOf(p) !== -1) {
        //linux
        args = [];
        if (config.numeric  !== false) args.push('-n');

        if (config.timeout  !== false) args.push('-w ' + config.timeout);

        if (config.minReply !== false) args.push('-c ' + config.minReply);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);
        log('System command: /bin/ping ' + args.join(' '));
        ls = cp.spawn('/bin/ping', args);
    } else if (p.match(/^win/)) {
        //windows
        args = [];
        if (config.minReply !== false) args.push('-n ' + config.minReply);

        if (config.timeout  !== false) args.push('-w ' + config.timeout * 1000);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);

        var allArgs = [
            '/s', // leave quotes as they are
            '/c', // run and exit
            // !!! order of c and s is important - c must come last!!!
            '"', // enforce starting quote
            process.env.SystemRoot + '\\system32\\ping.exe' // command itself. Notice that you'll have to pass it quoted if it contains spaces
        ].concat(args)
            .concat('"'); // enforce closing quote

        log('System command: ' + (process.env.comspec || 'cmd.exe') + ' ' + allArgs.join(' '));
        // switch the command to cmd shell instead of the original command
        ls = cp.spawn(process.env.comspec || 'cmd.exe', allArgs, {windowsVerbatimArguments: true});
    } else if (p === 'darwin' || p === 'freebsd') {
        //mac osx or freebsd
        args = [];
        if (config.numeric  !== false) args.push('-n');

        if (config.timeout  !== false) args.push('-t ' + config.timeout);

        if (config.minReply !== false) args.push('-c ' + config.minReply);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);
        log('System command: /sbin/ping ' + args.join(' '));
        ls = cp.spawn('/sbin/ping', args);
    } else {
        if (callback) callback('Your platform "' + p + '" is not supported');
    }

    ls.on('error', function (e) {
        if (callback) {
            callback(new Error(
                'ping.probe: there was an error while executing the ping program. check the path or permissions...'));
        }
        callback = null;
    });

    ls.stdout.on('data', function (data) {
        outstring += String(data);
    });

    ls.on('exit', function (code) {
        var lines  = outstring.split('\n');
        var ms     = null;
        var result = 1;

        for (var t = 0; t < lines.length; t++) {
            var m = regex.exec(lines[t]) || '';
            if (m !== '') {
                ms = m[1] || m[2];
                result = 0;
                break;
            }
        }

        if (p.match(/^win/)) {
            result = (ms !== null);
        } else {
            result = !code;
        }
        if (callback) {
            callback(null, {
                host:  addr,
                alive: result,
                ms:    ms
            });
        }
        callback = null;
    });
}

exports.probe = probe;
<commit_msg>Fixed arguments (#13)<commit_after>var cp  = require('child_process');
var p   = require('os').platform().toLowerCase();

function probe(addr, config, callback) {
    config = config || {};

    var ls  = null;
    var log = config.log || console.log;
    var outstring = '';

    config = {
        numeric:  config.numeric  === undefined ? true : config.numeric,
        timeout:  parseInt(config.timeout  === undefined ? 2    : config.timeout, 10),
        minReply: parseInt(config.minReply === undefined ? 1    : config.minReply, 10),
        extra:    config.extra || []
    };

    var args    = [];
    var xfamily = ['linux', 'sunos', 'unix'];
    var regex   = /=.*[<|=]([0-9]*).*TTL|ttl..*=([0-9\.]*)/;

    if (xfamily.indexOf(p) !== -1) {
        //linux
        args = [];
        if (config.numeric  !== false) args.push('-n');

        if (config.timeout  !== false) args.push('-w', config.timeout);

        if (config.minReply !== false) args.push('-c', config.minReply);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);
        log('System command: /bin/ping ' + args.join(' '));
        ls = cp.spawn('/bin/ping', args);
    } else if (p.match(/^win/)) {
        //windows
        args = [];
        if (config.minReply !== false) args.push('-n ' + config.minReply);

        if (config.timeout  !== false) args.push('-w ' + config.timeout * 1000);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);

        var allArgs = [
            '/s', // leave quotes as they are
            '/c', // run and exit
            // !!! order of c and s is important - c must come last!!!
            '"', // enforce starting quote
            process.env.SystemRoot + '\\system32\\ping.exe' // command itself. Notice that you'll have to pass it quoted if it contains spaces
        ].concat(args)
            .concat('"'); // enforce closing quote

        log('System command: ' + (process.env.comspec || 'cmd.exe') + ' ' + allArgs.join(' '));
        // switch the command to cmd shell instead of the original command
        ls = cp.spawn(process.env.comspec || 'cmd.exe', allArgs, {windowsVerbatimArguments: true});
    } else if (p === 'darwin' || p === 'freebsd') {
        //mac osx or freebsd
        args = [];
        if (config.numeric  !== false) args.push('-n');

        if (config.timeout  !== false) args.push('-t ' + config.timeout);

        if (config.minReply !== false) args.push('-c ' + config.minReply);

        if (config.extra    !== false) args = args.concat(config.extra);

        args.push(addr);
        log('System command: /sbin/ping ' + args.join(' '));
        ls = cp.spawn('/sbin/ping', args);
    } else {
        if (callback) callback('Your platform "' + p + '" is not supported');
    }

    ls.on('error', function (e) {
        if (callback) {
            callback(new Error(
                'ping.probe: there was an error while executing the ping program. check the path or permissions...'));
        }
        callback = null;
    });
    ls.stderr.on('data', function (data) {
        log('STDERR: '+ data);
    });

    ls.stdout.on('data', function (data) {
        outstring += String(data);
    });

    ls.on('exit', function (code) {
        var lines  = outstring.split('\n');
        var ms     = null;
        var result = 1;

        for (var t = 0; t < lines.length; t++) {
            var m = regex.exec(lines[t]) || '';
            if (m !== '') {
                ms = m[1] || m[2];
                result = 0;
                break;
            }
        }

        if (p.match(/^win/)) {
            result = (ms !== null);
        } else {
            result = !code;
        }
        if (callback) {
            callback(null, {
                host:  addr,
                alive: result,
                ms:    ms
            });
        }
        callback = null;
    });
}

exports.probe = probe;
<|endoftext|>