<commit_before>(function() {
	'use strict';

	var L = require('leaflet');
	L.Routing = L.Routing || {};
	L.extend(L.Routing, require('./L.Routing.Autocomplete'));
	L.extend(L.Routing, require('./L.Routing.Waypoint'));

	function selectInputText(input) {
		if (input.setSelectionRange) {
			// On iOS, select() doesn't work
			input.setSelectionRange(0, 9999);
		} else {
			// On at least IE8, setSeleectionRange doesn't exist
			input.select();
		}
	}

	L.Routing.Plan = L.Class.extend({
		includes: L.Mixin.Events,

		options: {
			dragStyles: [
				{color: 'black', opacity: 0.15, weight: 9},
				{color: 'white', opacity: 0.8, weight: 6},
				{color: 'red', opacity: 1, weight: 2, dashArray: '7,12'}
			],
			draggableWaypoints: true,
			addWaypoints: true,
			addButtonClassName: '',
			maxGeocoderTolerance: 200,
			autocompleteOptions: {},
			geocodersClassName: '',
			geocoderPlaceholder: function(i, numberWaypoints) {
				return i === 0 ?
					'Start' :
					(i < numberWaypoints - 1 ?
									'Via ' + i :
									'End');
			},
			geocoderClass: function() {
				return '';
			},
			createGeocoder: function() {
				var container = L.DomUtil.create('div', 'leaflet-routing-geocoder'),
					input = L.DomUtil.create('input', '', container),
					remove = L.DomUtil.create('span', 'leaflet-routing-remove-waypoint', container);

				return {
					container: container,
					input: input,
					closeButton: remove
				};
			},
			createMarker: function(i, wp) {
				var options = {
						draggable: this.draggableWaypoints
					},
				    marker = L.marker(wp.latLng, options);

				return marker;
			},
			waypointNameFallback: function(latLng) {
				var ns = latLng.lat < 0 ? 'S' : 'N',
				    ew = latLng.lng < 0 ? 'W' : 'E',
				    lat = (Math.round(Math.abs(latLng.lat) * 10000) / 10000).toString(),
				    lng = (Math.round(Math.abs(latLng.lng) * 10000) / 10000).toString();
				return ns + lat + ', ' + ew + lng;
			}
		},

		initialize: function(waypoints, options) {
			L.Util.setOptions(this, options);
			this._waypoints = [];
			this.setWaypoints(waypoints);
		},

		isReady: function() {
			var i;
			for (i = 0; i < this._waypoints.length; i++) {
				if (!this._waypoints[i].latLng) {
					return false;
				}
			}

			return true;
		},

		getWaypoints: function() {
			var i,
				wps = [];

			for (i = 0; i < this._waypoints.length; i++) {
				wps.push(this._waypoints[i]);
			}

			return wps;
		},

		setWaypoints: function(waypoints) {
			var args = [0, this._waypoints.length].concat(waypoints);
			this.spliceWaypoints.apply(this, args);
			return this;
		},

		spliceWaypoints: function() {
			var args = [arguments[0], arguments[1]],
			    i;

			for (i = 2; i < arguments.length; i++) {
				args.push(arguments[i] && arguments[i].hasOwnProperty('latLng') ? arguments[i] : L.Routing.waypoint(arguments[i]));
			}

			[].splice.apply(this._waypoints, args);

			this._updateMarkers();
			this._fireChanged.apply(this, args);

			// Make sure there's always at least two waypoints
			while (this._waypoints.length < 2) {
				this.spliceWaypoints(this._waypoints.length, 0, null);
			}
		},

		onAdd: function(map) {
			this._map = map;
			this._updateMarkers();
		},

		onRemove: function() {
			var i;
			this._removeMarkers();

			if (this._newWp) {
				for (i = 0; i < this._newWp.lines.length; i++) {
					this._map.removeLayer(this._newWp.lines[i]);
				}
			}

			delete this._map;
		},

		createGeocoders: function() {
			var container = L.DomUtil.create('div', 'leaflet-routing-geocoders ' + this.options.geocodersClassName),
				waypoints = this._waypoints,
			    i,
			    geocoderElem,
			    addWpBtn;

			this._geocoderContainer = container;
			this._geocoderElems = [];

			for (i = 0; i < waypoints.length; i++) {
				geocoderElem = this._createGeocoder(i);
				container.appendChild(geocoderElem.container);
				this._geocoderElems.push(geocoderElem);
			}

			addWpBtn = L.DomUtil.create('button', this.options.addButtonClassName, container);
			addWpBtn.setAttribute('type', 'button');
			addWpBtn.innerHTML = '+';
			if (this.options.addWaypoints) {
				L.DomEvent.addListener(addWpBtn, 'click', function() {
					this.spliceWaypoints(waypoints.length, 0, null);
				}, this);
			} else {
				addWpBtn.style.display = 'none';
			}

			this.on('waypointsspliced', this._updateGeocoders);

			return container;
		},

		_createGeocoder: function(i) {
			var nWps = this._waypoints.length,
				g = this.options.createGeocoder(i, nWps),
				closeButton = g.closeButton,
				geocoderInput = g.input,
				wp = this._waypoints[i];
			geocoderInput.setAttribute('placeholder', this.options.geocoderPlaceholder(i, nWps));
			geocoderInput.className = this.options.geocoderClass(i, nWps);

			this._updateWaypointName(i, geocoderInput);
			// This has to be here, or geocoder's value will not be properly
			// initialized.
			// TODO: look into why and make _updateWaypointName fix this.
			geocoderInput.value = wp.name;

			L.DomEvent.addListener(geocoderInput, 'click', function() {
				selectInputText(this);
			}, geocoderInput);

			if (closeButton) {
				L.DomEvent.addListener(closeButton, 'click', function() {
					if (i > 0 || this._waypoints.length > 2) {
						this.spliceWaypoints(i, 1);
					} else {
						this.spliceWaypoints(i, 1, new L.Routing.Waypoint());
					}
				}, this);
			}

			new L.Routing.Autocomplete(geocoderInput, function(r) {
					geocoderInput.value = r.name;
					wp.name = r.name;
					wp.latLng = r.center;
					this._updateMarkers();
					this._fireChanged();
					this._focusGeocoder(i + 1);
				}, this, L.extend({
					resultFn: this.options.geocoder.geocode,
					resultContext: this.options.geocoder,
					autocompleteFn: this.options.geocoder.suggest,
					autocompleteContext: this.options.geocoder
				}, this.options.autocompleteOptions));

			return g;
		},

		_updateGeocoders: function(e) {
			var newElems = [],
				addLast = e.index >= this._geocoderElems.length,
			    i,
			    geocoderElem,
			    beforeElem;

			// Determine where to insert geocoders for new waypoints
			if (addLast) {
				beforeElem =
					this._geocoderElems[this._geocoderElems.length].container.nextSibling;
			} else {
				beforeElem = this._geocoderElems[e.index].container;
			}

			// Insert new geocoders for new waypoints
			for (i = 0; i < e.added.length; i++) {
				geocoderElem = this._createGeocoder(e.index + i);
				this._geocoderContainer.insertBefore(geocoderElem.container, beforeElem);
				newElems.push(geocoderElem);
			}
			//newElems.reverse();

			for (i = e.index; i < e.index + e.nRemoved; i++) {
				this._geocoderContainer.removeChild(this._geocoderElems[i].container);
			}

			newElems.splice(0, 0, e.index, e.nRemoved);
			[].splice.apply(this._geocoderElems, newElems);

			for (i = 0; i < this._geocoderElems.length; i++) {
				this._geocoderElems[i].input.placeholder = this.options.geocoderPlaceholder(i, this._waypoints.length);
				this._geocoderElems[i].input.className = this.options.geocoderClass(i, this._waypoints.length);
			}
		},

		_updateGeocoder: function(i, geocoderElem) {
			var wp = this._waypoints[i],
			    value = wp && wp.name ? wp.name : '';
			if (geocoderElem) {
				geocoderElem.value = value;
			}
		},

		_updateWaypointName: function(i, geocoderElem, force) {
			var wp = this._waypoints[i],
					wpCoords;

			wp.name = wp.name || '';

			if (wp.latLng && (force || !wp.name)) {
				wpCoords = this.options.waypointNameFallback(wp.latLng);
				if (this.options.geocoder && this.options.geocoder.reverse) {
					this.options.geocoder.reverse(wp.latLng, 67108864 /* zoom 18 */, function(rs) {
						if (rs.length > 0 && rs[0].center.distanceTo(wp.latLng) < this.options.maxGeocoderTolerance) {
							wp.name = rs[0].name;
						} else {
							wp.name = wpCoords;
						}
						this._updateGeocoder(i, geocoderElem);
					}, this);
				} else {
					wp.name = wpCoords;
				}

				this._updateGeocoder(i, geocoderElem);
			}

		},

		_removeMarkers: function() {
			var i;
			if (this._markers) {
				for (i = 0; i < this._markers.length; i++) {
					if (this._markers[i]) {
						this._map.removeLayer(this._markers[i]);
					}
				}
			}
			this._markers = [];
		},

		_updateMarkers: function() {
			var i,
			    m;

			if (!this._map) {
				return;
			}

			this._removeMarkers();

			for (i = 0; i < this._waypoints.length; i++) {
				if (this._waypoints[i].latLng) {
					m = this.options.createMarker(i, this._waypoints[i], this._waypoints.length);
					if (m) {
						m.addTo(this._map);
						if (this.options.draggableWaypoints) {
							this._hookWaypointEvents(m, i);
						}
					}
				} else {
					m = null;
				}
				this._markers.push(m);
			}
		},

		_fireChanged: function() {
			this.fire('waypointschanged', {waypoints: this.getWaypoints()});

			if (arguments.length >= 2) {
				this.fire('waypointsspliced', {
					index: Array.prototype.shift.call(arguments),
					nRemoved: Array.prototype.shift.call(arguments),
					added: arguments
				});
			}
		},

		_hookWaypointEvents: function(m, i) {
			m.on('dragstart', function(e) {
				this.fire('waypointdragstart', this._createWaypointEvent(i, e));
			}, this);
			m.on('drag', function(e) {
				this._waypoints[i].latLng = e.target.getLatLng();
				this.fire('waypointdrag', this._createWaypointEvent(i, e));
			}, this);
			m.on('dragend', function(e) {
				this._waypoints[i].latLng = e.target.getLatLng();
				this._waypoints[i].name = '';
				this._updateWaypointName(i, this._geocoderElems && this._geocoderElems[i].input, true);
				this.fire('waypointdragend', this._createWaypointEvent(i, e));
				this._fireChanged();
			}, this);
		},

		_createWaypointEvent: function(i, e) {
			return {index: i, latlng: e.target.getLatLng()};
		},

		dragNewWaypoint: function(e) {
			var i;
			this._newWp = {
				afterIndex: e.afterIndex,
				marker: L.marker(e.latlng).addTo(this._map),
				lines: []
			};

			for (i = 0; i < this.options.dragStyles.length; i++) {
				this._newWp.lines.push(L.polyline([
					this._waypoints[e.afterIndex].latLng,
					e.latlng,
					this._waypoints[e.afterIndex + 1].latLng
				], this.options.dragStyles[i]).addTo(this._map));
			}

			this._markers.splice(e.afterIndex + 1, 0, this._newWp.marker);
			this._map.on('mousemove', this._onDragNewWp, this);
			this._map.on('mouseup', this._onWpRelease, this);
		},

		_onDragNewWp: function(e) {
			var i;
			this._newWp.marker.setLatLng(e.latlng);
			for (i = 0; i < this._newWp.lines.length; i++) {
				this._newWp.lines[i].spliceLatLngs(1, 1, e.latlng);
			}
		},

		_onWpRelease: function(e) {
			var i;
			this._map.off('mouseup', this._onWpRelease, this);
			this._map.off('mousemove', this._onDragNewWp, this);
			for (i = 0; i < this._newWp.lines.length; i++) {
				this._map.removeLayer(this._newWp.lines[i]);
			}
			this.spliceWaypoints(this._newWp.afterIndex + 1, 0, e.latlng);
			delete this._newWp;
		},

		_focusGeocoder: function(i) {
			var input;
			if (this._geocoderElems[i]) {
				input = this._geocoderElems[i].input;
				input.focus();
				selectInputText(input);
			} else {
				document.activeElement.blur();
			}
		}
	});

	L.Routing.plan = function(waypoints, options) {
		return new L.Routing.Plan(waypoints, options);
	};

	module.exports = L.Routing;
})();
<commit_msg>feat(plan): routeWhileDragging also used when adding a new waypoint by dragging the route line.<commit_after>(function() {
	'use strict';

	var L = require('leaflet');
	L.Routing = L.Routing || {};
	L.extend(L.Routing, require('./L.Routing.Autocomplete'));
	L.extend(L.Routing, require('./L.Routing.Waypoint'));

	function selectInputText(input) {
		if (input.setSelectionRange) {
			// On iOS, select() doesn't work
			input.setSelectionRange(0, 9999);
		} else {
			// On at least IE8, setSeleectionRange doesn't exist
			input.select();
		}
	}

	L.Routing.Plan = L.Class.extend({
		includes: L.Mixin.Events,

		options: {
			dragStyles: [
				{color: 'black', opacity: 0.15, weight: 9},
				{color: 'white', opacity: 0.8, weight: 6},
				{color: 'red', opacity: 1, weight: 2, dashArray: '7,12'}
			],
			draggableWaypoints: true,
			routeWhileDragging: false,
			addWaypoints: true,
			addButtonClassName: '',
			maxGeocoderTolerance: 200,
			autocompleteOptions: {},
			geocodersClassName: '',
			geocoderPlaceholder: function(i, numberWaypoints) {
				return i === 0 ?
					'Start' :
					(i < numberWaypoints - 1 ?
									'Via ' + i :
									'End');
			},
			geocoderClass: function() {
				return '';
			},
			createGeocoder: function() {
				var container = L.DomUtil.create('div', 'leaflet-routing-geocoder'),
					input = L.DomUtil.create('input', '', container),
					remove = L.DomUtil.create('span', 'leaflet-routing-remove-waypoint', container);

				return {
					container: container,
					input: input,
					closeButton: remove
				};
			},
			createMarker: function(i, wp) {
				var options = {
						draggable: this.draggableWaypoints
					},
				    marker = L.marker(wp.latLng, options);

				return marker;
			},
			waypointNameFallback: function(latLng) {
				var ns = latLng.lat < 0 ? 'S' : 'N',
				    ew = latLng.lng < 0 ? 'W' : 'E',
				    lat = (Math.round(Math.abs(latLng.lat) * 10000) / 10000).toString(),
				    lng = (Math.round(Math.abs(latLng.lng) * 10000) / 10000).toString();
				return ns + lat + ', ' + ew + lng;
			}
		},

		initialize: function(waypoints, options) {
			L.Util.setOptions(this, options);
			this._waypoints = [];
			this.setWaypoints(waypoints);
		},

		isReady: function() {
			var i;
			for (i = 0; i < this._waypoints.length; i++) {
				if (!this._waypoints[i].latLng) {
					return false;
				}
			}

			return true;
		},

		getWaypoints: function() {
			var i,
				wps = [];

			for (i = 0; i < this._waypoints.length; i++) {
				wps.push(this._waypoints[i]);
			}

			return wps;
		},

		setWaypoints: function(waypoints) {
			var args = [0, this._waypoints.length].concat(waypoints);
			this.spliceWaypoints.apply(this, args);
			return this;
		},

		spliceWaypoints: function() {
			var args = [arguments[0], arguments[1]],
			    i;

			for (i = 2; i < arguments.length; i++) {
				args.push(arguments[i] && arguments[i].hasOwnProperty('latLng') ? arguments[i] : L.Routing.waypoint(arguments[i]));
			}

			[].splice.apply(this._waypoints, args);

			this._updateMarkers();
			this._fireChanged.apply(this, args);

			// Make sure there's always at least two waypoints
			while (this._waypoints.length < 2) {
				this.spliceWaypoints(this._waypoints.length, 0, null);
			}
		},

		onAdd: function(map) {
			this._map = map;
			this._updateMarkers();
		},

		onRemove: function() {
			var i;
			this._removeMarkers();

			if (this._newWp) {
				for (i = 0; i < this._newWp.lines.length; i++) {
					this._map.removeLayer(this._newWp.lines[i]);
				}
			}

			delete this._map;
		},

		createGeocoders: function() {
			var container = L.DomUtil.create('div', 'leaflet-routing-geocoders ' + this.options.geocodersClassName),
				waypoints = this._waypoints,
			    i,
			    geocoderElem,
			    addWpBtn;

			this._geocoderContainer = container;
			this._geocoderElems = [];

			for (i = 0; i < waypoints.length; i++) {
				geocoderElem = this._createGeocoder(i);
				container.appendChild(geocoderElem.container);
				this._geocoderElems.push(geocoderElem);
			}

			addWpBtn = L.DomUtil.create('button', this.options.addButtonClassName, container);
			addWpBtn.setAttribute('type', 'button');
			addWpBtn.innerHTML = '+';
			if (this.options.addWaypoints) {
				L.DomEvent.addListener(addWpBtn, 'click', function() {
					this.spliceWaypoints(waypoints.length, 0, null);
				}, this);
			} else {
				addWpBtn.style.display = 'none';
			}

			this.on('waypointsspliced', this._updateGeocoders);

			return container;
		},

		_createGeocoder: function(i) {
			var nWps = this._waypoints.length,
				g = this.options.createGeocoder(i, nWps),
				closeButton = g.closeButton,
				geocoderInput = g.input,
				wp = this._waypoints[i];
			geocoderInput.setAttribute('placeholder', this.options.geocoderPlaceholder(i, nWps));
			geocoderInput.className = this.options.geocoderClass(i, nWps);

			this._updateWaypointName(i, geocoderInput);
			// This has to be here, or geocoder's value will not be properly
			// initialized.
			// TODO: look into why and make _updateWaypointName fix this.
			geocoderInput.value = wp.name;

			L.DomEvent.addListener(geocoderInput, 'click', function() {
				selectInputText(this);
			}, geocoderInput);

			if (closeButton) {
				L.DomEvent.addListener(closeButton, 'click', function() {
					if (i > 0 || this._waypoints.length > 2) {
						this.spliceWaypoints(i, 1);
					} else {
						this.spliceWaypoints(i, 1, new L.Routing.Waypoint());
					}
				}, this);
			}

			new L.Routing.Autocomplete(geocoderInput, function(r) {
					geocoderInput.value = r.name;
					wp.name = r.name;
					wp.latLng = r.center;
					this._updateMarkers();
					this._fireChanged();
					this._focusGeocoder(i + 1);
				}, this, L.extend({
					resultFn: this.options.geocoder.geocode,
					resultContext: this.options.geocoder,
					autocompleteFn: this.options.geocoder.suggest,
					autocompleteContext: this.options.geocoder
				}, this.options.autocompleteOptions));

			return g;
		},

		_updateGeocoders: function(e) {
			var newElems = [],
				addLast = e.index >= this._geocoderElems.length,
			    i,
			    geocoderElem,
			    beforeElem;

			// Determine where to insert geocoders for new waypoints
			if (addLast) {
				beforeElem =
					this._geocoderElems[this._geocoderElems.length].container.nextSibling;
			} else {
				beforeElem = this._geocoderElems[e.index].container;
			}

			// Insert new geocoders for new waypoints
			for (i = 0; i < e.added.length; i++) {
				geocoderElem = this._createGeocoder(e.index + i);
				this._geocoderContainer.insertBefore(geocoderElem.container, beforeElem);
				newElems.push(geocoderElem);
			}
			//newElems.reverse();

			for (i = e.index; i < e.index + e.nRemoved; i++) {
				this._geocoderContainer.removeChild(this._geocoderElems[i].container);
			}

			newElems.splice(0, 0, e.index, e.nRemoved);
			[].splice.apply(this._geocoderElems, newElems);

			for (i = 0; i < this._geocoderElems.length; i++) {
				this._geocoderElems[i].input.placeholder = this.options.geocoderPlaceholder(i, this._waypoints.length);
				this._geocoderElems[i].input.className = this.options.geocoderClass(i, this._waypoints.length);
			}
		},

		_updateGeocoder: function(i, geocoderElem) {
			var wp = this._waypoints[i],
			    value = wp && wp.name ? wp.name : '';
			if (geocoderElem) {
				geocoderElem.value = value;
			}
		},

		_updateWaypointName: function(i, geocoderElem, force) {
			var wp = this._waypoints[i],
					wpCoords;

			wp.name = wp.name || '';

			if (wp.latLng && (force || !wp.name)) {
				wpCoords = this.options.waypointNameFallback(wp.latLng);
				if (this.options.geocoder && this.options.geocoder.reverse) {
					this.options.geocoder.reverse(wp.latLng, 67108864 /* zoom 18 */, function(rs) {
						if (rs.length > 0 && rs[0].center.distanceTo(wp.latLng) < this.options.maxGeocoderTolerance) {
							wp.name = rs[0].name;
						} else {
							wp.name = wpCoords;
						}
						this._updateGeocoder(i, geocoderElem);
					}, this);
				} else {
					wp.name = wpCoords;
				}

				this._updateGeocoder(i, geocoderElem);
			}

		},

		_removeMarkers: function() {
			var i;
			if (this._markers) {
				for (i = 0; i < this._markers.length; i++) {
					if (this._markers[i]) {
						this._map.removeLayer(this._markers[i]);
					}
				}
			}
			this._markers = [];
		},

		_updateMarkers: function() {
			var i,
			    m;

			if (!this._map) {
				return;
			}

			this._removeMarkers();

			for (i = 0; i < this._waypoints.length; i++) {
				if (this._waypoints[i].latLng) {
					m = this.options.createMarker(i, this._waypoints[i], this._waypoints.length);
					if (m) {
						m.addTo(this._map);
						if (this.options.draggableWaypoints) {
							this._hookWaypointEvents(m, i);
						}
					}
				} else {
					m = null;
				}
				this._markers.push(m);
			}
		},

		_fireChanged: function() {
			this.fire('waypointschanged', {waypoints: this.getWaypoints()});

			if (arguments.length >= 2) {
				this.fire('waypointsspliced', {
					index: Array.prototype.shift.call(arguments),
					nRemoved: Array.prototype.shift.call(arguments),
					added: arguments
				});
			}
		},

		_hookWaypointEvents: function(m, i, trackMouseMove) {
			var eventLatLng = function(e) {
					return trackMouseMove ? e.latlng : e.target.getLatLng();
				},
				dragStart = L.bind(function(e) {
					this.fire('waypointdragstart', {index: i, latlng: eventLatLng(e)});
				}, this),
				drag = L.bind(function(e) {
					this._waypoints[i].latLng = eventLatLng(e);
					this.fire('waypointdrag', {index: i, latlng: eventLatLng(e)});
				}, this),
				dragEnd = L.bind(function(e) {
					this._waypoints[i].latLng = eventLatLng(e);
					this._waypoints[i].name = '';
					this._updateWaypointName(i, this._geocoderElems && this._geocoderElems[i].input, true);
					this.fire('waypointdragend', {index: i, latlng: eventLatLng(e)});
					this._fireChanged();
				}, this),
				mouseMove,
				mouseUp;

			if (trackMouseMove) {
				mouseMove = L.bind(function(e) {
					this._markers[i].setLatLng(e.latlng);
					drag(e);
				}, this);
				mouseUp = L.bind(function(e) {
					this._map.dragging.enable();
					this._map.off('mouseup', mouseUp);
					this._map.off('mousemove', mouseMove);
					dragEnd(e);
				}, this);
				this._map.dragging.disable();
				this._map.on('mousemove', mouseMove);
				this._map.on('mouseup', mouseUp);
				dragStart({latlng: this._waypoints[i].latLng});
			} else {
				m.on('dragstart', dragStart);
				m.on('drag', drag);
				m.on('dragend', dragEnd);
			}
		},

		dragNewWaypoint: function(e) {
			var newWpIndex = e.afterIndex + 1;
			if (this.options.routeWhileDragging) {
				this.spliceWaypoints(newWpIndex, 0, e.latlng);
				this._hookWaypointEvents(this._markers[newWpIndex], newWpIndex, true);
			} else {
				this._dragNewWaypoint(newWpIndex, e.latlng);
			}
		},

		_dragNewWaypoint: function(newWpIndex, initialLatLng) {
			var wp = new L.Routing.Waypoint(initialLatLng),
				prevWp = this._waypoints[newWpIndex - 1],
				nextWp = this._waypoints[newWpIndex],
				marker = this.options.createMarker(newWpIndex, wp, this._waypoints.length + 1),
				lines = [],
				mouseMove = L.bind(function(e) {
					var i;
					if (marker) {
						marker.setLatLng(e.latlng);
					}
					for (i = 0; i < lines.length; i++) {
						lines[i].spliceLatLngs(1, 1, e.latlng);
					}
				}, this),
				mouseUp = L.bind(function(e) {
					var i;
					if (marker) {
						this._map.removeLayer(marker);
					}
					for (i = 0; i < lines.length; i++) {
						this._map.removeLayer(lines[i]);
					}
					this._map.off('mousemove', mouseMove);
					this._map.off('mouseup', mouseUp);
					this.spliceWaypoints(newWpIndex, 0, e.latlng);
				}, this),
				i;

			if (marker) {
				marker.addTo(this._map);
			}

			for (i = 0; i < this.options.dragStyles.length; i++) {
				lines.push(L.polyline([prevWp.latLng, initialLatLng, nextWp.latLng],
					this.options.dragStyles[i]).addTo(this._map));
			}

			this._map.on('mousemove', mouseMove);
			this._map.on('mouseup', mouseUp);
		},

		_focusGeocoder: function(i) {
			var input;
			if (this._geocoderElems[i]) {
				input = this._geocoderElems[i].input;
				input.focus();
				selectInputText(input);
			} else {
				document.activeElement.blur();
			}
		}
	});

	L.Routing.plan = function(waypoints, options) {
		return new L.Routing.Plan(waypoints, options);
	};

	module.exports = L.Routing;
})();
<|endoftext|>


<commit_before>$(function(){
    MapasCulturais.EventOccurrenceManager.initMapTogglers('.toggle-mapa');
});

MapasCulturais.eventOccurrenceUpdateDialog = function ($caller){
    var $dialog = $($caller.data('dialog'));
    $dialog.find('h2').html($caller.data('dialog-title'));
    var template = MapasCulturais.TemplateManager.getTemplate('event-occurrence-form');
    var item = $caller.data('item') || {};

    if($caller.data('form-action') == 'edit'){
        item.formAction = item.editUrl;
        item.rule['freq_'+item.rule.frequency] = true;
        item.rule['monthly_'+item.rule.monthly] = true;

    }else{
        item.formAction = MapasCulturais.baseURL + 'eventOccurrence/create';
    }
    $dialog.find('.js-dialog-content').html(Mustache.render(template, item));

    MapasCulturais.EventOccurrenceManager.init($dialog.find('form'));
    MapasCulturais.EventHumanReadableManager.init($dialog.find('form'));

    $dialog.find('form').data('action', $caller.data('form-action'));

    MapasCulturais.Search.init('.js-search-occurrence-space');

    MapasCulturais.EventDates.init('.js-event-dates');

    var $startsAt = $dialog.find('form').find('.js-event-time');
    var $duration = $dialog.find('form').find('.js-event-duration');
    var $endsAt = $dialog.find('form').find('.js-event-end-time');

    $startsAt.mask('00:00', {
      onComplete: function(time) {
        var mtime = moment(time, 'HH:mm');
        var duration = $duration.val();

        if(mtime.isValid() && $.isNumeric(duration)) {
            $endsAt.val(mtime.add(duration, 'minutes').format('HH:mm'));
        }
      }});

    $duration.mask('99999999')
    $duration.change(function(event) {
        var mtime = moment($startsAt.val(), 'HH:mm');
        var duration = $(this).val();

        if(mtime.isValid() && $.isNumeric(duration)) {
            $endsAt.val(mtime.add(duration, 'minutes').format('HH:mm'));
        }
    });

    $endsAt.mask('00:00', {
      onComplete: function(time) {
        var mendtime = moment(time, 'HH:mm');
        var mtime = moment($startsAt.val(), 'HH:mm');

        if(mtime.isValid() && mendtime.isValid()) {
            $duration.val(Math.abs(mendtime.diff(mtime, 'minutes')));
        }
      }});
};

MapasCulturais.EventOccurrenceManager = {
    localeDateOptions : {
        locale : 'pt-BR',
        dateOptions : {
            //weekday: 'long',
            day: '2-digit', month:'2-digit', year:'numeric'
        }
    },
    formatDate : function (value){
        if(!value)
            return '';
        else
            return new Date(value + ' 12:00:00 GMT')
                .toLocaleDateString(this.localeDateOptions.locale, this.localeDateOptions.dateOptions);
    },
    init : function(selector) {
        $(selector).ajaxForm({
            success: function (response, statusText, xhr, $form)  {

                $form.find('.danger').not('.alert').remove();
                if(response.error){
                    var $element = null,
                        message;
                    for(i in response.data) {
                        message = response.data[i].join(', ').toLowerCase();

                        if(i == 'space') $element = $form.find('.js-space');
                        else $element = $form.find('[name="'+i+'"]').parents('.grupo-de-campos').find('label');
                        $element.append('');
                        //$form.find('[name="'+i+'"]')
                    }
                    $form.parent().scrollTop(0);
                    $form.find('div.alert.danger').html('Corrija os erros indicados abaixo.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);

                    return;

                }

                response.pending = xhr.status === 202;

                var isEditing = $form.data('action') == 'edit';
                var template = MapasCulturais.TemplateManager.getTemplate('event-occurrence-item');

                response.rule.screen_startsOn = MapasCulturais.EventOccurrenceManager.formatDate(response.rule.startsOn);
                response.rule.screen_until = MapasCulturais.EventOccurrenceManager.formatDate(response.rule.until);
                response.rule.screen_frequency = MapasCulturais.frequencies[response.rule.frequency];

                var $renderedData = $(Mustache.render(template, response));
                var $editBtn = $renderedData.find('.js-open-dialog');
                $editBtn.data('item', response);
                if(isEditing){
                    $('#event-occurrence-'+response.id).replaceWith($renderedData);
                }else{
                    $('.js-event-occurrence').append($renderedData);
                }
                MapasCulturais.Modal.initButtons($editBtn);
                $form.parents('.js-dialog').find('.js-close').click();

                //Por enquanto sempre inicializa o mapa
                //console.log('#occurrence-map-'+response.id, $('#occurrence-map-'+response.id), $('#occurrence-map-'+response.id).find('.toggle-mapa'));
                MapasCulturais.Map.initialize({mapSelector:'#occurrence-map-'+response.id,locateMeControl:false});
                MapasCulturais.EventOccurrenceManager.initMapTogglers($('#event-occurrence-'+response.id).find('.toggle-mapa'));

                if(xhr.status === 202){
                    MapasCulturais.Messages.alert('Sua requisição para criar a ocorrência do evento no espaço ' + response.space.name + ' foi enviada.');
                }

            },
            error: function(xhr, textStatus, errorThrown, $form) {
                $form.parent().scrollTop(0);

                if(xhr.status === 403){
                    $form.find('div.alert.danger').html('Você não tem permissão para criar eventos nesse espaço.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);
                }else{
                    $form.find('div.alert.danger').html('Erro inesperado.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);
                }
            },
            dataType:  'json',
            beforeSubmit: function(arr, $form, options) {

                if ($form.find('input[name="description"]').data('synced') != 1)
                    return confirm('As datas foram alteradas mas a descrição não. Tem certeza que deseja salvar?');

                return true;

            }
        });

        $(selector).find('.js-select-frequency').change(function(){
            $(selector).find('.js-freq-hide').not('.js-' + $(this).val())
                .hide()
                .find('input').not('[type=checkbox]').val('');
            $(selector).find('.js-freq-hide.js-' + $(this).val()).show();
            $(selector).find('.js-freq-hide').not('.js-' + $(this).val()).find('input[type=checkbox]').attr('checked', false);
        });
        $(selector).find('.js-select-frequency').change();
    },
    initMapTogglers : function(selector){
        $(selector).click(function() {
            var $map = $(this).closest('.regra').find('.mapa');
            MapasCulturais.reenableScrollWheelZoom = false;
            if($map.is(':visible')){
                $map.slideUp('fast');
                $(this).parent().find('.ver-mapa').show();
                $(this).parent().find('.ocultar-mapa').hide();

            }else{
                $map.slideDown('fast', function(){
                    $map.data('leaflet-map').invalidateSize();
                    $map.data('leaflet-map').scrollWheelZoom.disable();
                });
                $(this).parent().find('.ver-mapa').hide();
                $(this).parent().find('.ocultar-mapa').show();
            }

            return false;
        });
    }
};

MapasCulturais.EventDates = {
    init : function (selector) {
        $(selector).each(function(){
            var fieldSelector = '#'+$(this).attr('id');
            var altFieldSelector = $(this).data('alt-field') ? $(this).data('alt-field') : fieldSelector.replace('-visible', '');
            if($(altFieldSelector).length == 0){
                return;
            }
            $(this).datepicker({
                dateFormat: $(this).data('date-format') ? $(this).data('date-format') : 'dd/mm/yy',
                altFormat: $(this).data('alt-format') ? $(this).data('alt-format') : 'yy-mm-dd',
                altField: altFieldSelector
            });
        });
    }
};

MapasCulturais.EventHumanReadableManager = {

    init : function(selector) {

        var $form = $(selector);

        // Add onChange events to the fields
        $(selector).find('input[type="checkbox"], select[name="frequency"], #horario-de-inicio, .js-start-date, .js-end-date').change(function() {
            MapasCulturais.EventHumanReadableManager.updateSuggestion($form);
        });

        // If there are values, run it at init
        var date_s = $(selector).find('.js-start-date').val();
        var hour = $(selector).find('#horario-de-inicio').val();
        if (date_s && hour)
            MapasCulturais.EventHumanReadableManager.updateSuggestion($form);

        $(selector).find('.grupo-descricao-automatica > a').click(function() {
            $(selector).find('input[name="description"]').val( $(selector).find('#descricao-automatica').html() ).data('synced', 1);
        });

        //On init, we assume we are allways synced, even if the values are different, because the could have been manually edited
        $(selector).find('input[name="description"]').data('synced', 1);
    },
    updateSuggestion: function(selector) {
        var human = MapasCulturais.EventHumanReadableManager.getSuggestion(selector);
        $(selector).find('#descricao-automatica').html(human);
        if (human == $(selector).find('input[name="description"]').val())
            $(selector).find('input[name="description"]').data('synced', 1);
        else
            $(selector).find('input[name="description"]').data('synced', 0);
    },
    getSuggestion: function(selector) {

        var human = '';

        var date_s = $(selector).find('.js-start-date').val();
        var hour = $(selector).find('#horario-de-inicio').val();
        var frequency = $(selector).find('select[name="frequency"]').val();
        var date_e = $(selector).find('.js-end-date').val();
        var weekDays = [];
        $(selector).find('input[type="checkbox"]:checked').each(function() { if ($(this).is(':checked')) weekDays.push($(this).attr('name').replace(/[^\d]/g, '')) });

        //console.log( date_s);
        //console.log( hour);
        //console.log( frequency);
        //console.log( date_e);
        //console.log( weekDays);

        var mdate_s = false;
        var mdate_e = false;

        if (date_s) mdate_s = moment(date_s, 'DD/MM/YYYY');
        if (date_e) mdate_e = moment(date_e, 'DD/MM/YYYY');

        if (frequency == 'once') {
            if (!mdate_s) return '...';
            human += 'Dia ' + mdate_s.format('D [de] MMMM [de] YYYY');
        } else {

            if (!mdate_s || !mdate_e) return '...';

            if (frequency == 'daily') {
                human += 'Diariamente';
            } else if (frequency == 'weekly') {

                if (weekDays.length > 0) {

                    if (weekDays[0] == '0' || weekDays[0] == '6') {
                        human += 'Todo ';
                    } else {
                        human += 'Toda ';
                    }

                    var count = 1;
                    $.each(weekDays, function(i, v) {
                        var wformat = weekDays.length > 1 ? 'ddd' : 'dddd';
                        human += moment().day(v).format(wformat);
                        count ++;
                        if (count == weekDays.length)
                            human += ' e ';
                        else if (count < weekDays.length)
                            human += ', '
                    });
                }
            }

            if (mdate_s.year() != mdate_e.year()) {
                human += ' de ' + mdate_s.format('D [de] MMMM [de] YYYY') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
            } else {
                if (mdate_s.month() != mdate_e.month()) {
                    human += ' de ' + mdate_s.format('D [de] MMMM') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
                } else {
                    human += ' de ' + mdate_s.format('D') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
                }
            }

        }

        if (hour) {
            if (hour.substring(0,2) == '01')
                human += ' à ' + hour;
            else
                human += ' às ' + hour;
        }

        return human;

    }
};
<commit_msg>fix event occurrence endsAt field (refs #817)<commit_after>$(function(){
    MapasCulturais.EventOccurrenceManager.initMapTogglers('.toggle-mapa');
});

MapasCulturais.eventOccurrenceUpdateDialog = function ($caller){
    var $dialog = $($caller.data('dialog'));
    $dialog.find('h2').html($caller.data('dialog-title'));
    var template = MapasCulturais.TemplateManager.getTemplate('event-occurrence-form');
    var item = $caller.data('item') || {};

    if($caller.data('form-action') == 'edit'){
        item.formAction = item.editUrl;
        item.rule['freq_'+item.rule.frequency] = true;
        item.rule['monthly_'+item.rule.monthly] = true;

    }else{
        item.formAction = MapasCulturais.baseURL + 'eventOccurrence/create';
    }
    $dialog.find('.js-dialog-content').html(Mustache.render(template, item));

    MapasCulturais.EventOccurrenceManager.init($dialog.find('form'));
    MapasCulturais.EventHumanReadableManager.init($dialog.find('form'));

    $dialog.find('form').data('action', $caller.data('form-action'));

    MapasCulturais.Search.init('.js-search-occurrence-space');

    MapasCulturais.EventDates.init('.js-event-dates');

    var $startsAt = $dialog.find('form').find('.js-event-time');
    var $duration = $dialog.find('form').find('.js-event-duration');
    var $endsAt = $dialog.find('form').find('.js-event-end-time');

    $startsAt.mask('00:00', {
      onComplete: function(time) {
        var mtime = moment(time, 'HH:mm');
        var duration = $duration.val();

        if(mtime.isValid() && $.isNumeric(duration)) {
            $endsAt.val(mtime.add(duration, 'minutes').format('HH:mm'));
        }
      }});

    $duration.mask('99999999')
    $duration.change(function(event) {
        var mtime = moment($startsAt.val(), 'HH:mm');
        var duration = $(this).val();

        if(mtime.isValid() && $.isNumeric(duration)) {
            $endsAt.val(mtime.add(duration, 'minutes').format('HH:mm'));
        }
    });

    $endsAt.mask('00:00', {
      onComplete: function(time) {
        var mendtime = moment(time, 'HH:mm');
        var mtime = moment($startsAt.val(), 'HH:mm');

        if(mtime.isValid() && mendtime.isValid()) {
            if(mtime > mendtime){
                mendtime = mendtime.add('days', 1);
            }
            $duration.val(Math.abs(mendtime.diff(mtime, 'minutes')));
        }
      }});
};

MapasCulturais.EventOccurrenceManager = {
    localeDateOptions : {
        locale : 'pt-BR',
        dateOptions : {
            //weekday: 'long',
            day: '2-digit', month:'2-digit', year:'numeric'
        }
    },
    formatDate : function (value){
        if(!value)
            return '';
        else
            return new Date(value + ' 12:00:00 GMT')
                .toLocaleDateString(this.localeDateOptions.locale, this.localeDateOptions.dateOptions);
    },
    init : function(selector) {
        $(selector).ajaxForm({
            success: function (response, statusText, xhr, $form)  {

                $form.find('.danger').not('.alert').remove();
                if(response.error){
                    var $element = null,
                        message;
                    for(i in response.data) {
                        message = response.data[i].join(', ').toLowerCase();

                        if(i == 'space') $element = $form.find('.js-space');
                        else $element = $form.find('[name="'+i+'"]').parents('.grupo-de-campos').find('label');
                        $element.append('');
                        //$form.find('[name="'+i+'"]')
                    }
                    $form.parent().scrollTop(0);
                    $form.find('div.alert.danger').html('Corrija os erros indicados abaixo.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);

                    return;

                }

                response.pending = xhr.status === 202;

                var isEditing = $form.data('action') == 'edit';
                var template = MapasCulturais.TemplateManager.getTemplate('event-occurrence-item');

                response.rule.screen_startsOn = MapasCulturais.EventOccurrenceManager.formatDate(response.rule.startsOn);
                response.rule.screen_until = MapasCulturais.EventOccurrenceManager.formatDate(response.rule.until);
                response.rule.screen_frequency = MapasCulturais.frequencies[response.rule.frequency];

                var $renderedData = $(Mustache.render(template, response));
                var $editBtn = $renderedData.find('.js-open-dialog');
                $editBtn.data('item', response);
                if(isEditing){
                    $('#event-occurrence-'+response.id).replaceWith($renderedData);
                }else{
                    $('.js-event-occurrence').append($renderedData);
                }
                MapasCulturais.Modal.initButtons($editBtn);
                $form.parents('.js-dialog').find('.js-close').click();

                //Por enquanto sempre inicializa o mapa
                //console.log('#occurrence-map-'+response.id, $('#occurrence-map-'+response.id), $('#occurrence-map-'+response.id).find('.toggle-mapa'));
                MapasCulturais.Map.initialize({mapSelector:'#occurrence-map-'+response.id,locateMeControl:false});
                MapasCulturais.EventOccurrenceManager.initMapTogglers($('#event-occurrence-'+response.id).find('.toggle-mapa'));

                if(xhr.status === 202){
                    MapasCulturais.Messages.alert('Sua requisição para criar a ocorrência do evento no espaço ' + response.space.name + ' foi enviada.');
                }

            },
            error: function(xhr, textStatus, errorThrown, $form) {
                $form.parent().scrollTop(0);

                if(xhr.status === 403){
                    $form.find('div.alert.danger').html('Você não tem permissão para criar eventos nesse espaço.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);
                }else{
                    $form.find('div.alert.danger').html('Erro inesperado.')
                        .fadeIn(MapasCulturais.Messages.fadeOutSpeed)
                        .delay(MapasCulturais.Messages.delayToFadeOut)
                        .fadeOut(MapasCulturais.Messages.fadeOutSpeed);
                }
            },
            dataType:  'json',
            beforeSubmit: function(arr, $form, options) {

                if ($form.find('input[name="description"]').data('synced') != 1)
                    return confirm('As datas foram alteradas mas a descrição não. Tem certeza que deseja salvar?');

                return true;

            }
        });

        $(selector).find('.js-select-frequency').change(function(){
            $(selector).find('.js-freq-hide').not('.js-' + $(this).val())
                .hide()
                .find('input').not('[type=checkbox]').val('');
            $(selector).find('.js-freq-hide.js-' + $(this).val()).show();
            $(selector).find('.js-freq-hide').not('.js-' + $(this).val()).find('input[type=checkbox]').attr('checked', false);
        });
        $(selector).find('.js-select-frequency').change();
    },
    initMapTogglers : function(selector){
        $(selector).click(function() {
            var $map = $(this).closest('.regra').find('.mapa');
            MapasCulturais.reenableScrollWheelZoom = false;
            if($map.is(':visible')){
                $map.slideUp('fast');
                $(this).parent().find('.ver-mapa').show();
                $(this).parent().find('.ocultar-mapa').hide();

            }else{
                $map.slideDown('fast', function(){
                    $map.data('leaflet-map').invalidateSize();
                    $map.data('leaflet-map').scrollWheelZoom.disable();
                });
                $(this).parent().find('.ver-mapa').hide();
                $(this).parent().find('.ocultar-mapa').show();
            }

            return false;
        });
    }
};

MapasCulturais.EventDates = {
    init : function (selector) {
        $(selector).each(function(){
            var fieldSelector = '#'+$(this).attr('id');
            var altFieldSelector = $(this).data('alt-field') ? $(this).data('alt-field') : fieldSelector.replace('-visible', '');
            if($(altFieldSelector).length == 0){
                return;
            }
            $(this).datepicker({
                dateFormat: $(this).data('date-format') ? $(this).data('date-format') : 'dd/mm/yy',
                altFormat: $(this).data('alt-format') ? $(this).data('alt-format') : 'yy-mm-dd',
                altField: altFieldSelector
            });
        });
    }
};

MapasCulturais.EventHumanReadableManager = {

    init : function(selector) {

        var $form = $(selector);

        // Add onChange events to the fields
        $(selector).find('input[type="checkbox"], select[name="frequency"], #horario-de-inicio, .js-start-date, .js-end-date').change(function() {
            MapasCulturais.EventHumanReadableManager.updateSuggestion($form);
        });

        // If there are values, run it at init
        var date_s = $(selector).find('.js-start-date').val();
        var hour = $(selector).find('#horario-de-inicio').val();
        if (date_s && hour)
            MapasCulturais.EventHumanReadableManager.updateSuggestion($form);

        $(selector).find('.grupo-descricao-automatica > a').click(function() {
            $(selector).find('input[name="description"]').val( $(selector).find('#descricao-automatica').html() ).data('synced', 1);
        });

        //On init, we assume we are allways synced, even if the values are different, because the could have been manually edited
        $(selector).find('input[name="description"]').data('synced', 1);
    },
    updateSuggestion: function(selector) {
        var human = MapasCulturais.EventHumanReadableManager.getSuggestion(selector);
        $(selector).find('#descricao-automatica').html(human);
        if (human == $(selector).find('input[name="description"]').val())
            $(selector).find('input[name="description"]').data('synced', 1);
        else
            $(selector).find('input[name="description"]').data('synced', 0);
    },
    getSuggestion: function(selector) {

        var human = '';

        var date_s = $(selector).find('.js-start-date').val();
        var hour = $(selector).find('#horario-de-inicio').val();
        var frequency = $(selector).find('select[name="frequency"]').val();
        var date_e = $(selector).find('.js-end-date').val();
        var weekDays = [];
        $(selector).find('input[type="checkbox"]:checked').each(function() { if ($(this).is(':checked')) weekDays.push($(this).attr('name').replace(/[^\d]/g, '')) });

        //console.log( date_s);
        //console.log( hour);
        //console.log( frequency);
        //console.log( date_e);
        //console.log( weekDays);

        var mdate_s = false;
        var mdate_e = false;

        if (date_s) mdate_s = moment(date_s, 'DD/MM/YYYY');
        if (date_e) mdate_e = moment(date_e, 'DD/MM/YYYY');

        if (frequency == 'once') {
            if (!mdate_s) return '...';
            human += 'Dia ' + mdate_s.format('D [de] MMMM [de] YYYY');
        } else {

            if (!mdate_s || !mdate_e) return '...';

            if (frequency == 'daily') {
                human += 'Diariamente';
            } else if (frequency == 'weekly') {

                if (weekDays.length > 0) {

                    if (weekDays[0] == '0' || weekDays[0] == '6') {
                        human += 'Todo ';
                    } else {
                        human += 'Toda ';
                    }

                    var count = 1;
                    $.each(weekDays, function(i, v) {
                        var wformat = weekDays.length > 1 ? 'ddd' : 'dddd';
                        human += moment().day(v).format(wformat);
                        count ++;
                        if (count == weekDays.length)
                            human += ' e ';
                        else if (count < weekDays.length)
                            human += ', '
                    });
                }
            }

            if (mdate_s.year() != mdate_e.year()) {
                human += ' de ' + mdate_s.format('D [de] MMMM [de] YYYY') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
            } else {
                if (mdate_s.month() != mdate_e.month()) {
                    human += ' de ' + mdate_s.format('D [de] MMMM') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
                } else {
                    human += ' de ' + mdate_s.format('D') + ' a ' + mdate_e.format('D [de] MMMM [de] YYYY');
                }
            }

        }

        if (hour) {
            if (hour.substring(0,2) == '01')
                human += ' à ' + hour;
            else
                human += ' às ' + hour;
        }

        return human;

    }
};
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import Toggle from 'react-toggle';
import 'react-toggle/style.css';

import Wrapper from 'common/Wrapper';
import ReadableList from 'common/ReadableList';
import Contributor from 'Main/Contributor';

import CORE_CHANGELOG from '../CHANGELOG';

let _stringChangelogDeprecatedWarningSent = false;

class Changelog extends React.PureComponent {
  static propTypes = {
    changelog: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
    limit: PropTypes.number,
    includeCore: PropTypes.bool,
  };
  static defaultProps = {
    includeCore: true,
  };
  constructor() {
    super();
    this.state = {
      includeCore: true,
    };
  }

  render() {
    const { changelog, limit, includeCore } = this.props;

    if (typeof changelog === 'string') {
      if (!_stringChangelogDeprecatedWarningSent) {
        console.error('String based changelogs are deprecated. Convert the changelog to an array basis for more features, see the Holy Paladin changelog for an example.');
        _stringChangelogDeprecatedWarningSent = true;
      }
      return (

            {changelog
              .trim()
              .split('\n')
              .filter((_, i) => !limit || i < limit)
              .map((change, i) => (

              ))}

      );
    }
    if (changelog instanceof Array) {
      const mergedChangelog = includeCore && this.state.includeCore ? [ ...CORE_CHANGELOG, ...changelog ].sort((a, b) => b.date - a.date) : changelog;
      return (

          {includeCore && (

              <Toggle
                defaultChecked={this.state.includeCore}
                icons={false}
                onChange={event => this.setState({ includeCore: event.target.checked })}
                id="core-entries-toggle"
              />

                <dfn data-tip="Turn this off to only see changes to this spec's implementation.">Shared changes</dfn>

          )}

            {mergedChangelog
              .filter((_, i) => !limit || i < limit)
              .map((entry, i) => {
                const { date, changes, contributors } = entry;
                const isFromCoreChangelog = CORE_CHANGELOG.includes(entry);
                // The index of the entry provides us with a unique never changing key, which speeds up the Shared Changes toggle
                const index = isFromCoreChangelog ? CORE_CHANGELOG.indexOf(entry) : changelog.indexOf(entry);

                return (
                  <li
                    key={isFromCoreChangelog ? `core-${index}` : `spec-${index}`}
                    className={`flex wrapable ${includeCore && isFromCoreChangelog ? 'text-muted' : ''}`}
                  >

                      {date.toLocaleDateString()}

                      {changes}

                      <ReadableList>
                        {contributors.map(contributor => <Contributor {...contributor} />)}
                      </ReadableList>

                );
              })}

      );
    }
    return null;
  }
}

export default Changelog;
<commit_msg>Fix eslint<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import Toggle from 'react-toggle';
import 'react-toggle/style.css';

import ReadableList from 'common/ReadableList';
import Contributor from 'Main/Contributor';

import CORE_CHANGELOG from '../CHANGELOG';

let _stringChangelogDeprecatedWarningSent = false;

class Changelog extends React.PureComponent {
  static propTypes = {
    changelog: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
    limit: PropTypes.number,
    includeCore: PropTypes.bool,
  };
  static defaultProps = {
    includeCore: true,
  };
  constructor() {
    super();
    this.state = {
      includeCore: true,
    };
  }

  render() {
    const { changelog, limit, includeCore } = this.props;

    if (typeof changelog === 'string') {
      if (!_stringChangelogDeprecatedWarningSent) {
        console.error('String based changelogs are deprecated. Convert the changelog to an array basis for more features, see the Holy Paladin changelog for an example.');
        _stringChangelogDeprecatedWarningSent = true;
      }
      return (

            {changelog
              .trim()
              .split('\n')
              .filter((_, i) => !limit || i < limit)
              .map((change, i) => (

              ))}

      );
    }
    if (changelog instanceof Array) {
      const mergedChangelog = includeCore && this.state.includeCore ? [ ...CORE_CHANGELOG, ...changelog ].sort((a, b) => b.date - a.date) : changelog;
      return (

          {includeCore && (

              <Toggle
                defaultChecked={this.state.includeCore}
                icons={false}
                onChange={event => this.setState({ includeCore: event.target.checked })}
                id="core-entries-toggle"
              />

                <dfn data-tip="Turn this off to only see changes to this spec's implementation.">Shared changes</dfn>

          )}

            {mergedChangelog
              .filter((_, i) => !limit || i < limit)
              .map((entry, i) => {
                const { date, changes, contributors } = entry;
                const isFromCoreChangelog = CORE_CHANGELOG.includes(entry);
                // The index of the entry provides us with a unique never changing key, which speeds up the Shared Changes toggle
                const index = isFromCoreChangelog ? CORE_CHANGELOG.indexOf(entry) : changelog.indexOf(entry);

                return (
                  <li
                    key={isFromCoreChangelog ? `core-${index}` : `spec-${index}`}
                    className={`flex wrapable ${includeCore && isFromCoreChangelog ? 'text-muted' : ''}`}
                  >

                      {date.toLocaleDateString()}

                      {changes}

                      <ReadableList>
                        {contributors.map(contributor => <Contributor {...contributor} />)}
                      </ReadableList>

                );
              })}

      );
    }
    return null;
  }
}

export default Changelog;
<|endoftext|>


<commit_before>// Copyright 2013-2014 Danny Yates

//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at

//        http://www.apache.org/licenses/LICENSE-2.0

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

var crypto = require('crypto'),
    url = require('url'),
    util = require('util'),
    validUrl = require('valid-url'),
    uri = require('uri-js'),
    messageFactory = require('./messageFactory'),
    Association = require('./association'),
    MemoryAssociationStore = require('./memoryAssociationStore');

// TODO: These should all be configurable by the client
var PROVIDER_ENDPOINT = 'http://localhost:3000/'; // TODO
var DEFAULT_ASSOCIATION_EXPIRY = 30; // TODO
var COOKIE = 'oidp'; // TODO
var LOGIN_URL = url.resolve(PROVIDER_ENDPOINT, 'login'); // TODO

var DH_MODULUS_HEX = 'DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61E' +
                     'F75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D268370557' +
                     '7D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E382' +
                     '6634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB';

var DH_MODULUS_B64 = new Buffer(DH_MODULUS_HEX, 'hex').toString('base64');

var OPENID_NS = 'http://specs.openid.net/auth/2.0';  // TODO duplicated

var HTML_DISCOVERY_RESPONSE_TEMPLATE = '<!DOCTYPE html>\
<html>\
<head>\
<title>OpenID Provider</title>\
<link rel="openid2.provider" href="%s">\
</head>\
<body>\
</body>\
</html>\
';

var HTML_VALIDATION_RESPONSE_TEMPLATE = '<!DOCTYPE html>\
<html>\
<head>\
<title>OpenID Provider</title>\
<link rel="openid2.provider" href="%s">\
<link rel="openid2.local_id" href="%s">\
</head>\
<body>\
</body>\
</html>\
';

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

    var self = this;
    var associationStore = new MemoryAssociationStore();  // TODO

    this.express = function() {
        return middleware;
    }

    this.providerEndpoint = function() {
        return PROVIDER_ENDPOINT;
    }

    this.sendHtmlDiscoveryResponse = function(req, res, user, next) {
        res.type('text/html');
        if (typeof user === 'function') {
            res.end(util.format(HTML_DISCOVERY_RESPONSE_TEMPLATE, this.providerEndpoint()));
        } else {
            // TODO is this needed any more?
            res.end(util.format(HTML_VALIDATION_RESPONSE_TEMPLATE, this.providerEndpoint(), localId(user)));
        }
    }

    this.completeAuth = function(req, res, username, next) {
        checkIdSetupComplete(req, res, username, next);
    }

    function middleware(req, res, next) {
        if (req.method === 'POST') {
            var request = messageFactory.fromBody(req);

            if (request.ns !== OPENID_NS) return next(); // not OpenID (4.1.2)

            if (request.mode === 'associate') {  // 8.1
                return associate(request, req, res, next);
            }

            // unknown direct request
            sendDirectResponseError(res, {
                error: 'Unknown or unsupported direct request'
            });
        } else if (req.method === 'GET') {
            var request = messageFactory.fromQueryArgs(req);

            if (request.ns !== OPENID_NS) return next(); // not OpenID (4.1.2)

            if (request.mode === 'checkid_setup') {
                return checkIdSetup(request, res, next);
            }

            // unknown indirect request
            sendIndirectResponseError(request, res, 'Unknown or unsupported indirect request');
        } else {
            next();
        }
    }

    function associate(request, req, res, next) {
        if (request.session_type === 'no-encryption') {
            if (req.secure) {
                unencryptedAssociation(request, res, next);
            } else {
                // 8.1.1, 8.4.1
                unsupportedAssociation(res, 'Cannot create a "no-encryption" session without using HTTPS');
            }
        } else if (request.session_type === 'DH-SHA1') {
            diffieHellmanAssociate(request, res, 'sha1', next);
        } else if (request.session_type === 'DH-SHA256') {
            diffieHellmanAssociate(request, res, 'sha256', next);
        } else {
            unsupportedAssociation(res, 'Session type not recognised: ' + request.session_type);
        }
    }

    function unencryptedAssociation(request, res, next) {
        createMac(request.assoc_type, function(err, macBuffer, hashAlgorithm) {
            if (err) return internalError(err, res, next);
            if (!macBuffer) return unsupportedAssociation(res, 'Association type not recognised: ' + request.assoc_type);

            var association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);

            associationStore.put(association, function(err) {
                if (err) return internalError(err, res, next);

                var response = {
                    assoc_handle: association.handle,
                    session_type: request.session_type,
                    assoc_type: request.assoc_type,
                    expires_in: DEFAULT_ASSOCIATION_EXPIRY,
                    mac_key: macBuffer.toString('base64')
                }

                sendDirectResponse(res, response);
            });
        });
    }

    function diffieHellmanAssociate(request, res, dhHash, next) {
        createMac(request.assoc_type, function(err, macBuffer, hashAlgorithm) {
            if (err) return internalError(err, res, next);
            if (!macBuffer) return unsupportedAssociation(res, 'Association type not recognised: ' + request.assoc_type);

            var dh = crypto.createDiffieHellman(request.dh_modulus || DH_MODULUS_B64, 'base64');
            var publicKeyBase64 = dh.generateKeys('base64');
            var secretKeyBinary = dh.computeSecret(request.dh_consumer_public, 'base64');
            var hash = crypto.createHash(hashAlgorithm);

            hash.update(btwoc(secretKeyBinary));

            var association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);
            var encodedMac = xor(hash.digest(), macBuffer);

            associationStore.put(association, function(err) {
                if (err) return internalError(err, res, next);

                var response = {
                    assoc_handle: association.handle,
                    session_type: request.session_type,
                    assoc_type: request.assoc_type,
                    expires_in: DEFAULT_ASSOCIATION_EXPIRY,
                    dh_server_public: publicKeyBase64,
                    enc_mac_key: encodedMac.toString('base64')
                }

                sendDirectResponse(res, response);
            });
        });
    }

    function createMac(assocType, next) {
        if (assocType === 'HMAC-SHA1') {
            crypto.randomBytes(20, function(err, buffer) {
                next(err, buffer, 'sha1');
            });
        } else if (assocType === 'HMAC-SHA256') {
            crypto.randomBytes(32, function(err, buffer) {
                next(err, buffer, 'sha256');
            });
        } else {
            next();  // pass undefined buffer back to caller
        }
    }

    function unsupportedAssociation(res, message) {
        // 8.2.4
        sendDirectResponseError(res, {
            error: message,
            error_code: 'unsupported-type',
            session_type: 'DH-SHA256',
            assoc_type: 'HMAC-SHA256'
        });
    }

    // 4.2
    function btwoc(buffer) {
        // if the most significant byte doesn't have it's most significant bit set, we're good
        if (buffer[0] <= 127) return buffer;

        // make a new buffer which is a copy of the old with an extra 0x00 on the front
        var result = new Buffer(buffer.length + 1);
        result[0] = 0;
        buffer.copy(result, 1);
        return result;
    }

    function xor(a, b) {
        var result = new Buffer(a.length);

        for (var i = 0; i < a.length; i++) {
            result[i] = a[i] ^ b[i];
        };

        return result;
    }

    function checkIdSetup(request, res, message) {
        if (!request.return_to && !request.realm) {
            return sendIndirectResponseError(request, res, 'checkid_setup must specify one (or both) of return_to and realm');  // 9.1
        }

        if (request.realm) {
            var wildcardRealm = false;

            // Validate realm. 9.2
            var parsedRealm = uri.parse(request.realm);  // Use a third-party parser instead of the node URL module to get wildcard support

            if (parsedRealm.errors.length > 0) return sendIndirectResponseError(request, res, 'Invalid realm');

            if (parsedRealm.fragment) return sendIndirectResponseError(request, res, 'Realm cannot contain a fragment');

            if (parsedRealm.host.slice(0, 2) === '*.') {
                // replace the wildcard realm with a non-wildcard
                parsedRealm.host = parsedRealm.host.slice(2);
                wildcardRealm = true;
            }

            parsedRealm = uri.normalize(parsedRealm);

            // Now revalidate with something that specifically understands HTTP and HTTPS URLs (instead of general URIs)
            if (!validUrl.isWebUri(uri.serialize(parsedRealm))) return sendIndirectResponseError(request, res, 'Invalid realm');

            if (request.return_to) {
                var parsedReturnTo = uri.normalize(uri.parse(request.return_to));

                // The schemes and ports must be equal
                if (parsedRealm.scheme !== parsedReturnTo.scheme || parsedRealm.port !== parsedReturnTo.port) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }

                // The pathnames must be equal or else the return_to pathname must be a "sub-directory" of the realm pathname. 9.2
                if (!(parsedRealm.pathname === parsedReturnTo.pathname || parsedReturnTo.pathname.indexOf(parsedRealm.pathname + '/') === 0)) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }

                // Hostnames match or the return_to hostname must END with '.'+realm hostname IFF realm was wildcarded
                if (!(parsedRealm.hostname === parsedReturnTo.hostname ||
                     (wildcardRealm && parsedReturnTo.hostname.substr(parsedReturnTo.hostname.length - parsedRealm.hostname.length - 1) === '.' + parsedRealm.hostname))) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }
            }
        }

        // TODO: check if already logged in

        res.cookie(COOKIE, request, {
            signed: true,
            maxAge: 5 * 60 * 1000   // 5 mins
        });

        res.redirect(302, LOGIN_URL);
    }

    function checkIdSetupComplete(req, res, username, next) {
        var request = req.signedCookies[COOKIE],
            association,
            response = {
                mode: 'id_res',
                op_endpoint: PROVIDER_ENDPOINT,
                claimed_id: request.claimed_id,
                identity: localId(username),
                return_to: request.return_to
            };

        if (!request) return res.send(404);

        // Cookie is signed, so shouldn't be tampered with. Do some basic checks just to be sure.
        if (request.ns !== OPENID_NS || request.mode !== 'checkid_setup') return sendIndirectResponseError('Cookie tampering! Oh my!');

        function checkAssociation() {
            var deleteHandle;

            if (request.assoc_handle) {
                associationStore.get(request.assoc_handle, function(err, a) {
                    if (err) return internalError(err, res, next);

                    var remove = false;

                    association = a;

                    if (association && association.expires < Date.now()) {
                        // Association expired
                        remove = true;
                        association = undefined;  // Proceed as if no handle specified. s10
                    }

                    if (!association) {
                        // Not found or expired
                        response.invalidate_handle = request.assoc_handle;
                    }

                    if (remove) {
                        associationStore.delete(request.assoc_handle, function(err) {
                            if (err) return internalError(err, res, next);

                            privateAssociation();
                        });
                    } else {
                        privateAssociation();
                    }
                });
            } else {
                privateAssociation();
            }
        }

        function privateAssociation() {
            if (!association) {
                // Make a "private association". The spec is vague here. For example, how do we know the client supports the algorithm we choose?
                createMac('HMAC-SHA256', function(err, macBuffer, hashAlgorithm) {
                    if (err) return internalError(err, res, next);

                    association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);

                    associationStore.put(association, function(err) {
                        if (err) return internalError(err, res, next);

                        signResponse();
                    });
                });
            } else {
                signResponse();
            }
        }

        function signResponse() {
            response.assoc_handle = association.handle;

            var hmac = crypto.createHmac(association.algorithm, new Buffer(association.secret, 'base64'));
            var message = messageFactory.toForm(response);

            hmac.update(message.body);

            response.sig = hmac.digest('base64');
            response.signed = message.fields.join(',');

            sendResponse();
        }

        function sendResponse() {
            sendIndirectResponse(request, res, response);
        }

        checkAssociation();
    }

    function sendIndirectResponseError(request, res, message) {
        // 5.2.3
        var response = {
            mode: 'error',
            error: message
        }

        sendIndirectResponse(request, res, response);
    }

    function sendIndirectResponse(request, res, response) {
        if (!request.return_to || !validUrl.isWebUri(request.return_to)) {
            // No return_to, or not a valid URL.
            return res.send(400);
        }

        // TODO some or all of this belongs in messageFactory

        var returnToUrl = url.parse(request.return_to, true);  // 5.2.3

        returnToUrl.query['openid.ns'] = OPENID_NS;
        for (var field in response) {
            // 4.1.3
            returnToUrl.query['openid.' + field] = response[field];
        }

        res.redirect(302, url.format(returnToUrl));  // 5.2.1
    }

    function sendDirectResponseError(res, response) {
        // 5.1.2.2
        res.status(400);
        sendDirectResponse(res, response);
    }

    function sendDirectResponse(res, response) {
        res.type('text/plain');  // 5.1.2
        res.send(messageFactory.toForm(response).body);
    }

    function internalError(err, res, next) {
        // Return a 500 to the caller and pass the error to the Express error handler
        res.type('text/plain');
        res.send(500, err.message || err);
        next(err);
    }

    function localId(username) {
        return url.resolve(PROVIDER_ENDPOINT, 'users/' + username.toLowerCase())
    }
}

exports = module.exports = OpenIDProvider;
<commit_msg>Correctly add query params to indirect error<commit_after>// Copyright 2013-2014 Danny Yates

//    Licensed under the Apache License, Version 2.0 (the "License");
//    you may not use this file except in compliance with the License.
//    You may obtain a copy of the License at

//        http://www.apache.org/licenses/LICENSE-2.0

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

var crypto = require('crypto'),
    url = require('url'),
    util = require('util'),
    validUrl = require('valid-url'),
    uri = require('uri-js'),
    messageFactory = require('./messageFactory'),
    Association = require('./association'),
    MemoryAssociationStore = require('./memoryAssociationStore');

// TODO: These should all be configurable by the client
var PROVIDER_ENDPOINT = 'http://localhost:3000/'; // TODO
var DEFAULT_ASSOCIATION_EXPIRY = 30; // TODO
var COOKIE = 'oidp'; // TODO
var LOGIN_URL = url.resolve(PROVIDER_ENDPOINT, 'login'); // TODO

var DH_MODULUS_HEX = 'DCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61E' +
                     'F75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D268370557' +
                     '7D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E382' +
                     '6634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB';

var DH_MODULUS_B64 = new Buffer(DH_MODULUS_HEX, 'hex').toString('base64');

var OPENID_NS = 'http://specs.openid.net/auth/2.0';  // TODO duplicated

var HTML_DISCOVERY_RESPONSE_TEMPLATE = '<!DOCTYPE html>\
<html>\
<head>\
<title>OpenID Provider</title>\
<link rel="openid2.provider" href="%s">\
</head>\
<body>\
</body>\
</html>\
';

var HTML_VALIDATION_RESPONSE_TEMPLATE = '<!DOCTYPE html>\
<html>\
<head>\
<title>OpenID Provider</title>\
<link rel="openid2.provider" href="%s">\
<link rel="openid2.local_id" href="%s">\
</head>\
<body>\
</body>\
</html>\
';

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

    var self = this;
    var associationStore = new MemoryAssociationStore();  // TODO

    this.express = function() {
        return middleware;
    }

    this.providerEndpoint = function() {
        return PROVIDER_ENDPOINT;
    }

    this.sendHtmlDiscoveryResponse = function(req, res, user, next) {
        res.type('text/html');
        if (typeof user === 'function') {
            res.end(util.format(HTML_DISCOVERY_RESPONSE_TEMPLATE, this.providerEndpoint()));
        } else {
            // TODO is this needed any more?
            res.end(util.format(HTML_VALIDATION_RESPONSE_TEMPLATE, this.providerEndpoint(), localId(user)));
        }
    }

    this.completeAuth = function(req, res, username, next) {
        checkIdSetupComplete(req, res, username, next);
    }

    function middleware(req, res, next) {
        if (req.method === 'POST') {
            var request = messageFactory.fromBody(req);

            if (request.ns !== OPENID_NS) return next(); // not OpenID (4.1.2)

            if (request.mode === 'associate') {  // 8.1
                return associate(request, req, res, next);
            }

            // unknown direct request
            sendDirectResponseError(res, {
                error: 'Unknown or unsupported direct request'
            });
        } else if (req.method === 'GET') {
            var request = messageFactory.fromQueryArgs(req);

            if (request.ns !== OPENID_NS) return next(); // not OpenID (4.1.2)

            if (request.mode === 'checkid_setup') {
                return checkIdSetup(request, res, next);
            }

            // unknown indirect request
            sendIndirectResponseError(request, res, 'Unknown or unsupported indirect request');
        } else {
            next();
        }
    }

    function associate(request, req, res, next) {
        if (request.session_type === 'no-encryption') {
            if (req.secure) {
                unencryptedAssociation(request, res, next);
            } else {
                // 8.1.1, 8.4.1
                unsupportedAssociation(res, 'Cannot create a "no-encryption" session without using HTTPS');
            }
        } else if (request.session_type === 'DH-SHA1') {
            diffieHellmanAssociate(request, res, 'sha1', next);
        } else if (request.session_type === 'DH-SHA256') {
            diffieHellmanAssociate(request, res, 'sha256', next);
        } else {
            unsupportedAssociation(res, 'Session type not recognised: ' + request.session_type);
        }
    }

    function unencryptedAssociation(request, res, next) {
        createMac(request.assoc_type, function(err, macBuffer, hashAlgorithm) {
            if (err) return internalError(err, res, next);
            if (!macBuffer) return unsupportedAssociation(res, 'Association type not recognised: ' + request.assoc_type);

            var association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);

            associationStore.put(association, function(err) {
                if (err) return internalError(err, res, next);

                var response = {
                    assoc_handle: association.handle,
                    session_type: request.session_type,
                    assoc_type: request.assoc_type,
                    expires_in: DEFAULT_ASSOCIATION_EXPIRY,
                    mac_key: macBuffer.toString('base64')
                }

                sendDirectResponse(res, response);
            });
        });
    }

    function diffieHellmanAssociate(request, res, dhHash, next) {
        createMac(request.assoc_type, function(err, macBuffer, hashAlgorithm) {
            if (err) return internalError(err, res, next);
            if (!macBuffer) return unsupportedAssociation(res, 'Association type not recognised: ' + request.assoc_type);

            var dh = crypto.createDiffieHellman(request.dh_modulus || DH_MODULUS_B64, 'base64');
            var publicKeyBase64 = dh.generateKeys('base64');
            var secretKeyBinary = dh.computeSecret(request.dh_consumer_public, 'base64');
            var hash = crypto.createHash(hashAlgorithm);

            hash.update(btwoc(secretKeyBinary));

            var association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);
            var encodedMac = xor(hash.digest(), macBuffer);

            associationStore.put(association, function(err) {
                if (err) return internalError(err, res, next);

                var response = {
                    assoc_handle: association.handle,
                    session_type: request.session_type,
                    assoc_type: request.assoc_type,
                    expires_in: DEFAULT_ASSOCIATION_EXPIRY,
                    dh_server_public: publicKeyBase64,
                    enc_mac_key: encodedMac.toString('base64')
                }

                sendDirectResponse(res, response);
            });
        });
    }

    function createMac(assocType, next) {
        if (assocType === 'HMAC-SHA1') {
            crypto.randomBytes(20, function(err, buffer) {
                next(err, buffer, 'sha1');
            });
        } else if (assocType === 'HMAC-SHA256') {
            crypto.randomBytes(32, function(err, buffer) {
                next(err, buffer, 'sha256');
            });
        } else {
            next();  // pass undefined buffer back to caller
        }
    }

    function unsupportedAssociation(res, message) {
        // 8.2.4
        sendDirectResponseError(res, {
            error: message,
            error_code: 'unsupported-type',
            session_type: 'DH-SHA256',
            assoc_type: 'HMAC-SHA256'
        });
    }

    // 4.2
    function btwoc(buffer) {
        // if the most significant byte doesn't have it's most significant bit set, we're good
        if (buffer[0] <= 127) return buffer;

        // make a new buffer which is a copy of the old with an extra 0x00 on the front
        var result = new Buffer(buffer.length + 1);
        result[0] = 0;
        buffer.copy(result, 1);
        return result;
    }

    function xor(a, b) {
        var result = new Buffer(a.length);

        for (var i = 0; i < a.length; i++) {
            result[i] = a[i] ^ b[i];
        };

        return result;
    }

    function checkIdSetup(request, res, message) {
        if (!request.return_to && !request.realm) {
            return sendIndirectResponseError(request, res, 'checkid_setup must specify one (or both) of return_to and realm');  // 9.1
        }

        if (request.realm) {
            var wildcardRealm = false;

            // Validate realm. 9.2
            var parsedRealm = uri.parse(request.realm);  // Use a third-party parser instead of the node URL module to get wildcard support

            if (parsedRealm.errors.length > 0) return sendIndirectResponseError(request, res, 'Invalid realm');

            if (parsedRealm.fragment) return sendIndirectResponseError(request, res, 'Realm cannot contain a fragment');

            if (parsedRealm.host.slice(0, 2) === '*.') {
                // replace the wildcard realm with a non-wildcard
                parsedRealm.host = parsedRealm.host.slice(2);
                wildcardRealm = true;
            }

            parsedRealm = uri.normalize(parsedRealm);

            // Now revalidate with something that specifically understands HTTP and HTTPS URLs (instead of general URIs)
            if (!validUrl.isWebUri(uri.serialize(parsedRealm))) return sendIndirectResponseError(request, res, 'Invalid realm');

            if (request.return_to) {
                var parsedReturnTo = uri.normalize(uri.parse(request.return_to));

                // The schemes and ports must be equal
                if (parsedRealm.scheme !== parsedReturnTo.scheme || parsedRealm.port !== parsedReturnTo.port) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }

                // The pathnames must be equal or else the return_to pathname must be a "sub-directory" of the realm pathname. 9.2
                if (!(parsedRealm.pathname === parsedReturnTo.pathname || parsedReturnTo.pathname.indexOf(parsedRealm.pathname + '/') === 0)) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }

                // Hostnames match or the return_to hostname must END with '.'+realm hostname IFF realm was wildcarded
                if (!(parsedRealm.hostname === parsedReturnTo.hostname ||
                     (wildcardRealm && parsedReturnTo.hostname.substr(parsedReturnTo.hostname.length - parsedRealm.hostname.length - 1) === '.' + parsedRealm.hostname))) {
                    return sendIndirectResponseError(request, res, 'The return_to URL does not match the realm');
                }
            }
        }

        // TODO: check if already logged in

        res.cookie(COOKIE, request, {
            signed: true,
            maxAge: 5 * 60 * 1000   // 5 mins
        });

        res.redirect(302, LOGIN_URL);
    }

    function checkIdSetupComplete(req, res, username, next) {
        var request = req.signedCookies[COOKIE],
            association,
            response = {
                mode: 'id_res',
                op_endpoint: PROVIDER_ENDPOINT,
                claimed_id: request.claimed_id,
                identity: localId(username),
                return_to: request.return_to
            };

        if (!request) return res.send(404);

        // Cookie is signed, so shouldn't be tampered with. Do some basic checks just to be sure.
        if (request.ns !== OPENID_NS || request.mode !== 'checkid_setup') return sendIndirectResponseError('Cookie tampering! Oh my!');

        function checkAssociation() {
            var deleteHandle;

            if (request.assoc_handle) {
                associationStore.get(request.assoc_handle, function(err, a) {
                    if (err) return internalError(err, res, next);

                    var remove = false;

                    association = a;

                    if (association && association.expires < Date.now()) {
                        // Association expired
                        remove = true;
                        association = undefined;  // Proceed as if no handle specified. s10
                    }

                    if (!association) {
                        // Not found or expired
                        response.invalidate_handle = request.assoc_handle;
                    }

                    if (remove) {
                        associationStore.delete(request.assoc_handle, function(err) {
                            if (err) return internalError(err, res, next);

                            privateAssociation();
                        });
                    } else {
                        privateAssociation();
                    }
                });
            } else {
                privateAssociation();
            }
        }

        function privateAssociation() {
            if (!association) {
                // Make a "private association". The spec is vague here. For example, how do we know the client supports the algorithm we choose?
                createMac('HMAC-SHA256', function(err, macBuffer, hashAlgorithm) {
                    if (err) return internalError(err, res, next);

                    association = new Association(hashAlgorithm, macBuffer.toString('base64'), DEFAULT_ASSOCIATION_EXPIRY);

                    associationStore.put(association, function(err) {
                        if (err) return internalError(err, res, next);

                        signResponse();
                    });
                });
            } else {
                signResponse();
            }
        }

        function signResponse() {
            response.assoc_handle = association.handle;

            var hmac = crypto.createHmac(association.algorithm, new Buffer(association.secret, 'base64'));
            var message = messageFactory.toForm(response);

            hmac.update(message.body);

            response.sig = hmac.digest('base64');
            response.signed = message.fields.join(',');

            sendResponse();
        }

        function sendResponse() {
            sendIndirectResponse(request, res, response);
        }

        checkAssociation();
    }

    function sendIndirectResponseError(request, res, message) {
        // 5.2.3
        var response = {
            mode: 'error',
            error: message
        }

        sendIndirectResponse(request, res, response);
    }

    function sendIndirectResponse(request, res, response) {
        if (!request.return_to || !validUrl.isWebUri(request.return_to)) {
            // No return_to, or not a valid URL.
            return res.send(400);
        }

        // TODO some or all of this belongs in messageFactory

        var returnToUrl = url.parse(request.return_to, true);  // 5.2.3

        returnToUrl.query['openid.ns'] = OPENID_NS;
        for (var field in response) {
            // 4.1.3
            returnToUrl.query['openid.' + field] = response[field];
        }
        delete returnToUrl.search;

        res.redirect(302, url.format(returnToUrl));  // 5.2.1
    }

    function sendDirectResponseError(res, response) {
        // 5.1.2.2
        res.status(400);
        sendDirectResponse(res, response);
    }

    function sendDirectResponse(res, response) {
        res.type('text/plain');  // 5.1.2
        res.send(messageFactory.toForm(response).body);
    }

    function internalError(err, res, next) {
        // Return a 500 to the caller and pass the error to the Express error handler
        res.type('text/plain');
        res.send(500, err.message || err);
        next(err);
    }

    function localId(username) {
        return url.resolve(PROVIDER_ENDPOINT, 'users/' + username.toLowerCase())
    }
}

exports = module.exports = OpenIDProvider;
<|endoftext|>