<commit_before>/**
* WebSocket API
*/

var url = require('url'), querystring = require('querystring'), crypto = require('crypto'),
	jsonStableStringify = require('json-stable-stringify');

var Device = require(__dirname + '/../models/device'), helper = require(__dirname + '/../models/helper');

// Array for WebSocket Connections
var wsConnections = [];

// ----

module.exports = {

	/**
	 * Method for called when the new client was connected
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 */
	onWsConnection: function (ws) {

		var self = module.exports;

		// Get an upgraded url from HTTP connection
		var location = url.parse(ws.upgradeReq.url, true).href;

		// Get an IP address
		ws.ipAddress = helper.getClientIPAddress(ws.upgradeReq);

		// Check a permission of the client
		self._checkClientPermission(ws, location, function (ws, location, device) { // Client is valid

			if (!device.approvedAt) { // Unapproved device
				console.log('WebSocket client connected', ws.ipAddress, location, 'DeviceID=' + device.id, 'Unapproved');
			} else { // Approved device
				console.log('WebSocket client connected', ws.ipAddress, location, 'DeviceID=' + device.id, 'Approved');
			}

			// Accept the connection
			wsConnections.push(ws);

			// Set the incoming event handlers to connection
			ws.on('message', function (message) {
				self._onWsMessage(ws, message);
			});

			// Set the disconnection event handlers to connection
			ws.on('error', function () {
				console.log('WebSocket client disconnected by error');
				wsConnections = wsConnections.filter(function (conn, i) {
					return (conn === ws) ? false : true;
				});
			});
			ws.on('close', function () {
				console.log('WebSocket client disconnected');
				wsConnections = wsConnections.filter(function (conn, i) {
					return (conn === ws) ? false : true;
				});
			});

		}, function (ws, location, error_text) { // Client is invalid

			console.log('WebSocket client connected but blocked', ws.ipAddress, location, error_text);
			ws.close();

		});

	},

	/**
	 * Get the connection by the Device ID
	 * @param  {Number} device_id Device ID
	 * @return {Object}           Connection instance of ws library
	 */
	getConnectionByDeviceId: function (device_id) {

		for (var i = 0, l = wsConnections.length; i < l; i++) {
			try {
				if (wsConnections[i].deviceId == device_id) {
					return wsConnections[i];
				}
			} catch (e) {
				console.log(e.toString);
			}
		}

		return null;

	},

	/**
	 * Check a permission of the connected client
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {String} location Upgraded url from HTTP connection
	 * @param  {Function} cb_success Callback for Successful - function (ws, location, device)
	 * @param  {Function} cb_success Callback for Failed - function (ws, location, error_text)
	 */
	_checkClientPermission: function (ws, location, cb_success, cb_failed) {

		var self = module.exports;

		// Parse a query string from the url
		var query = {};
		if (location.match(/\?([\S\s]+)$/)) {
			query = querystring.parse(RegExp.$1);
		}

		// Get the Device Name string from query
		var name = query.deviceName || null;

		// Get the Device Token string from query
		var token = query.deviceToken || '';

		// Find the device from the database
		var db = helper.getDB();
		db.query('SELECT * FROM Device WHERE deviceToken = ?;', [token], function (err, rows) {

			if (err) {
				cb_failed(ws, location, err.toString());
				return;

			} else if (rows.length == 0) { // Device was not registered

				if (name == null) {
					cb_failed(ws, location, 'This anonymous device could not registered', token);
					return;
				}

				// If necessary, generate the token string
				var is_generated_token = false;
				if (token == '') {
					token = 'DEVICETOKEN_' + name + '_' + Math.random(999999) + '_' + new Date().toString();
					token = crypto.createHash('sha256').update(token).digest('hex');
					is_generated_token = true;
				}

				// Add the unapproved device
				console.log('This device are unapproved', name, token);
				var now = new Date();
				db.query('INSERT Device(name, deviceToken, createdAt, lastConnectedAt) VALUES (?, ?, ?, ?);',
				[name, token, now, now],
				function (err, result) {

					if (err) {
						cb_failed(ws, location, err.toString());
						return;
					}

					// Add the device ID to the WebSocket connection
					ws.deviceId = result.insertId;

					// Send a token
					if (is_generated_token) {
						ws.send(JSON.stringify({
							cmd: '_changeToken',
							token: token
						}));
					}

					// Done
					var device = {
						name: name,
						deviceToken: token,
						createdAt: now,
						lastConnectedAt: now
					};
					cb_success(ws, location, device);

				});
				return;

			}

			// Add the device ID to the WebSocket connection
			var device = rows[0];
			ws.deviceId = device.id;

			// Update the lastConnectedAt field on the database
			db.query('UPDATE Device SET lastConnectedAt = ? WHERE id = ?;', [new Date(), device.id],
			function (err, result) {

				// Done
				cb_success(ws, location, device);

			});

		});

	},

	/**
	 * Method for called when the message was received
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {String} message Received message
	 */
	_onWsMessage: function (ws, message) {

		var self = module.exports;

		console.log('WebSocket message received', message);

		var data = {};
		try {
			data = JSON.parse(message);
		} catch (e) {
			console.warn('Unknown message', e.toString());
		}

		if (data.cmd == '_sendManifests') {
			self._registerDeviceManifest(ws, ws.deviceId, data.commands, data.typeName);
		}

	},

	/**
	 * Get the device object from Device ID
	 * @param  {Number} device_id Devce 5D
	 * @param  {Function} callback function - function (error_text, device)
	 */
	_getDevice: function (device_id, callback) {

		var self = module.exports;

		var db = helper.getDB();
		db.query('SELECT * FROM Device WHERE id = ?', [device_id], function (err, rows) {

			if (err || rows.length == 0) {
				callback('Could not get the device', null);
				return;
			}

			callback(null, rows[0]);

		});

	},

	/**
	 * Register the commands definition to the database.
	 * It also generate the Device Type.
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {Number} device_id Devce ID
	 * @param  {Object} commands Commands
	 * @param  {String} base_type_name Device type name which self-reported from the device
	 */
	_registerDeviceManifest: function (ws, device_id, commands, base_type_name) {

		var self = module.exports;

		var db = helper.getDB();

		self._getDevice(device_id, function (error_text, device) {

			if (error_text) {
				console.warn(error_text);
				return false;
			}

			// Make a json from commands
			var commands_json = jsonStableStringify(commands, function (a, b) {
				return a.key < b.key ? 1 : -1;
			});

			// Get the number of the same DeviceType with this device
			var type_id = device.deviceTypeId;
			db.query('SELECT * FROM Device WHERE deviceTypeId = ?;', [type_id], function (err, rows) {

				if (err) {
					console.warn(err.toString());
					return;
				}

				// Get the number of related device from the DeviceType
				var num_of_related_device = rows.length;

				// Get the current DeviceType from the database
				db.query('SELECT * FROM DeviceType WHERE id = ?;', [type_id], function (err, rows) {

					if (err) {
						console.warn(err.toString());
						return;
					}

					// Compare the commands of the current DeviceType with device
					var device_type = rows[0] || {};
					if (rows.length == 1 && rows[0].commands != null && rows[0].commands == commands_json) {
						console.log('Commands are not changed', 'DeviceId=' + device_id, 'DeviceTypeId=' + type_id);
						return false; // commands are not changed
					}

					// Find the similar Device Type
					db.query('SELECT * FROM DeviceType WHERE baseName = ?;', [base_type_name], function (err, rows) { // Commands was matched

						if (err) {
							console.warn(err.toString());
							return false;
						}

						// Compare the commands of the DeviceType with the device
						for (var i = 0; i < rows.length; i++) {

							var similar_type_id = rows[i].id;

							if (rows[i].commands != null && rows[i].commands == commands_json) { // Commands was matched to similar device type

								console.log('DeviceType has been merged', 'DeviceId=' + device_id, 'DeviceTypeId(Src)=' + type_id, 'DeviceTypeId(Dst)=' + similar_type_id);

								// Change the deviceTypeId of the device to the similar DeviceType
								db.query('UPDATE Device SET commands = ?, deviceTypeId = ? WHERE id = ?;', [commands_json, similar_type_id, device_id],
								function (err, result) {
									if (err) console.warn('Could not update the device type', err.toString());
								});

								// Merge the DeviceType to the similar DeviceType
								db.query('DELETE FROM DeviceType WHERE id = ?;', [type_id],
								function (err, result) {
									if (err) console.warn('Could not merge the device type', err.toString());
								});

								return true;

							}

						}

						// Create or Update of DeviceType
						if (num_of_related_device == 1) {

							console.log('Device Type has been updated', 'DeviceId=' + device_id, 'DeviceTypeId=' + type_id, 'DeviceTypeName=' + device_type.name);

							// Update the commands of type device type
							db.query('UPDATE DeviceType SET baseName = ?, commands = ? WHERE id = ?;', [base_type_name, commands_json, type_id],
							function (err, result) {

								if (err) console.warn('Could not update the device type', err.toString());

							});

							// Update the commands of the device
							db.query('UPDATE Device SET commands = ? WHERE id = ?;', [commands_json, device_id],
							function (err, result) {

								if (err) console.warn(err.toString());

							});

						} else {

							// Make a name of the new device type
							var type_name = base_type_name || device.name;
							type_name += '-' + crypto.createHash('sha1').update(commands_json).digest('hex');

							// Make the new device type
							console.log('Device Type has been created', 'DeviceId=' + device_id, 'DeviceTypeId=NEW', 'DeviceTypeName=' + type_name);
							db.query('INSERT INTO DeviceType(name, baseName, commands) VALUES(?, ?, ?);', [type_name, base_type_name, commands_json],
							function (err, result) {

								if (err) {
									console.warn('Could not make the device type', err.toString());
									return;
								}

								type_id = result.insertId;

								// Update the device type ID and commands of the device
								db.query('UPDATE Device SET deviceTypeId = ?, commands = ? WHERE id = ?;', [type_id, commands_json, device_id],
								function (err, result) {

									if (err) console.warn(err.toString());

								});

							});

						}

					});

				});

			});

		});

	}

};
<commit_msg>Fix: _sendManifest<commit_after>/**
* WebSocket API
*/

var url = require('url'), querystring = require('querystring'), crypto = require('crypto'),
	jsonStableStringify = require('json-stable-stringify');

var Device = require(__dirname + '/../models/device'), helper = require(__dirname + '/../models/helper');

// Array for WebSocket Connections
var wsConnections = [];

// ----

module.exports = {

	/**
	 * Method for called when the new client was connected
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 */
	onWsConnection: function (ws) {

		var self = module.exports;

		// Get an upgraded url from HTTP connection
		var location = url.parse(ws.upgradeReq.url, true).href;

		// Get an IP address
		ws.ipAddress = helper.getClientIPAddress(ws.upgradeReq);

		// Set the incoming event handlers to connection
		ws.on('message', function (message) {
			self._onWsMessage(ws, message);
		});

		// Set the disconnection event handlers to connection
		ws.on('error', function () {
			console.log('WebSocket client disconnected by error');
			wsConnections = wsConnections.filter(function (conn, i) {
				return (conn === ws) ? false : true;
			});
		});
		ws.on('close', function () {
			console.log('WebSocket client disconnected');
			wsConnections = wsConnections.filter(function (conn, i) {
				return (conn === ws) ? false : true;
			});
		});

		// Check a permission of the client
		self._checkClientPermission(ws, location, function (ws, location, device) { // Client is valid

			if (!device.approvedAt) { // Unapproved device
				console.log('WebSocket client connected', ws.ipAddress, location, 'DeviceID=' + device.id, 'Unapproved');
			} else { // Approved device
				console.log('WebSocket client connected', ws.ipAddress, location, 'DeviceID=' + device.id, 'Approved');
			}

			// Accept the connection
			wsConnections.push(ws);

		}, function (ws, location, error_text) { // Client is invalid

			console.log('WebSocket client connected but blocked', ws.ipAddress, location, error_text);
			ws.close();

		});

	},

	/**
	 * Get the connection by the Device ID
	 * @param  {Number} device_id Device ID
	 * @return {Object}           Connection instance of ws library
	 */
	getConnectionByDeviceId: function (device_id) {

		for (var i = 0, l = wsConnections.length; i < l; i++) {
			try {
				if (wsConnections[i].deviceId == device_id) {
					return wsConnections[i];
				}
			} catch (e) {
				console.log(e.toString);
			}
		}

		return null;

	},

	/**
	 * Check a permission of the connected client
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {String} location Upgraded url from HTTP connection
	 * @param  {Function} cb_success Callback for Successful - function (ws, location, device)
	 * @param  {Function} cb_success Callback for Failed - function (ws, location, error_text)
	 */
	_checkClientPermission: function (ws, location, cb_success, cb_failed) {

		var self = module.exports;

		// Parse a query string from the url
		var query = {};
		if (location.match(/\?([\S\s]+)$/)) {
			query = querystring.parse(RegExp.$1);
		}

		// Get the Device Name string from query
		var name = query.deviceName || null;

		// Get the Device Token string from query
		var token = query.deviceToken || '';

		// Find the device from the database
		var db = helper.getDB();
		db.query('SELECT * FROM Device WHERE deviceToken = ?;', [token], function (err, rows) {

			if (err) {
				cb_failed(ws, location, err.toString());
				return;

			} else if (rows.length == 0) { // Device was not registered

				if (name == null) {
					cb_failed(ws, location, 'This anonymous device could not registered', token);
					return;
				}

				// If necessary, generate the token string
				var is_generated_token = false;
				if (!token || token == 'null' || token.length == 0) {
					token = 'DEVICETOKEN_' + name + '_' + Math.random(999999) + '_' + new Date().toString();
					token = crypto.createHash('sha256').update(token).digest('hex');
					is_generated_token = true;
					console.log('Generated new token: ' + token);
				}

				// Add the unapproved device
				console.log('This device are unapproved', name, token);
				var now = new Date();
				db.query('INSERT Device(name, deviceToken, createdAt, lastConnectedAt) VALUES (?, ?, ?, ?);',
				[name, token, now, now],
				function (err, result) {

					if (err) {
						cb_failed(ws, location, err.toString());
						return;
					}

					// Add the device ID to the WebSocket connection
					ws.deviceId = result.insertId;

					// Send a token
					if (is_generated_token) {
						ws.send(JSON.stringify({
							cmd: '_changeToken',
							args: {
								deviceToken: token
							}
						}));
					}

					// Done
					var device = {
						name: name,
						deviceToken: token,
						createdAt: now,
						lastConnectedAt: now
					};
					cb_success(ws, location, device);

				});
				return;

			}

			// Add the device ID to the WebSocket connection
			var device = rows[0];
			ws.deviceId = device.id;

			// Update the lastConnectedAt field on the database
			db.query('UPDATE Device SET lastConnectedAt = ? WHERE id = ?;', [new Date(), device.id],
			function (err, result) {

				// Done
				cb_success(ws, location, device);

			});

		});

	},

	/**
	 * Method for called when the message was received
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {String} message Received message
	 */
	_onWsMessage: function (ws, message) {

		var self = module.exports;

		console.log('WebSocket message received', message);

		var data = {};
		try {
			data = JSON.parse(message);
		} catch (e) {
			console.warn('Unknown message', e.toString());
		}

		if (data.cmd == '_sendManifest') {
			console.log('Manifest received: ' + JSON.stringify(data.args));
			self._registerDeviceManifest(ws, ws.deviceId, data.args.commands, data.args.deviceType);
		}

	},

	/**
	 * Get the device object from Device ID
	 * @param  {Number} device_id Devce 5D
	 * @param  {Function} callback function - function (error_text, device)
	 */
	_getDevice: function (device_id, callback) {

		var self = module.exports;

		var db = helper.getDB();
		db.query('SELECT * FROM Device WHERE id = ?', [device_id], function (err, rows) {

			if (err || rows.length == 0) {
				callback('Could not get the device', null);
				return;
			}

			callback(null, rows[0]);

		});

	},

	/**
	 * Register the commands definition to the database.
	 * It also generate the Device Type.
	 * @param  {Object} ws Instance of ws library - https://github.com/websockets/ws
	 * @param  {Number} device_id Devce ID
	 * @param  {Object} commands Commands
	 * @param  {String} base_type_name Device type name which self-reported from the device
	 */
	_registerDeviceManifest: function (ws, device_id, commands, base_type_name) {

		var self = module.exports;

		var db = helper.getDB();

		self._getDevice(device_id, function (error_text, device) {

			if (error_text) {
				console.warn(error_text);
				return false;
			}

			// Make a json from commands
			var commands_json = jsonStableStringify(commands, function (a, b) {
				return a.key < b.key ? 1 : -1;
			});

			// Get the number of the same DeviceType with this device
			var type_id = device.deviceTypeId;
			db.query('SELECT * FROM Device WHERE deviceTypeId = ?;', [type_id], function (err, rows) {

				if (err) {
					console.warn(err.toString());
					return;
				}

				// Get the number of related device from the DeviceType
				var num_of_related_device = rows.length;

				// Get the current DeviceType from the database
				db.query('SELECT * FROM DeviceType WHERE id = ?;', [type_id], function (err, rows) {

					if (err) {
						console.warn(err.toString());
						return;
					}

					// Compare the commands of the current DeviceType with device
					var device_type = rows[0] || {};
					if (rows.length == 1 && rows[0].commands != null && rows[0].commands == commands_json) {
						console.log('Commands are not changed', 'DeviceId=' + device_id, 'DeviceTypeId=' + type_id);
						return false; // commands are not changed
					}

					// Find the similar Device Type
					db.query('SELECT * FROM DeviceType WHERE baseName = ?;', [base_type_name], function (err, rows) { // Commands was matched

						if (err) {
							console.warn(err.toString());
							return false;
						}

						// Compare the commands of the DeviceType with the device
						for (var i = 0; i < rows.length; i++) {

							var similar_type_id = rows[i].id;

							if (rows[i].commands != null && rows[i].commands == commands_json) { // Commands was matched to similar device type

								console.log('DeviceType has been merged', 'DeviceId=' + device_id, 'DeviceTypeId(Src)=' + type_id, 'DeviceTypeId(Dst)=' + similar_type_id);

								// Change the deviceTypeId of the device to the similar DeviceType
								db.query('UPDATE Device SET commands = ?, deviceTypeId = ? WHERE id = ?;', [commands_json, similar_type_id, device_id],
								function (err, result) {
									if (err) console.warn('Could not update the device type', err.toString());
								});

								// Merge the DeviceType to the similar DeviceType
								db.query('DELETE FROM DeviceType WHERE id = ?;', [type_id],
								function (err, result) {
									if (err) console.warn('Could not merge the device type', err.toString());
								});

								return true;

							}

						}

						// Create or Update of DeviceType
						if (num_of_related_device == 1) {

							console.log('Device Type has been updated', 'DeviceId=' + device_id, 'DeviceTypeId=' + type_id, 'DeviceTypeName=' + device_type.name);

							// Update the commands of type device type
							db.query('UPDATE DeviceType SET baseName = ?, commands = ? WHERE id = ?;', [base_type_name, commands_json, type_id],
							function (err, result) {

								if (err) console.warn('Could not update the device type', err.toString());

							});

							// Update the commands of the device
							db.query('UPDATE Device SET commands = ? WHERE id = ?;', [commands_json, device_id],
							function (err, result) {

								if (err) console.warn(err.toString());

							});

						} else {

							// Make a name of the new device type
							var type_name = base_type_name || device.name;
							type_name += '-' + crypto.createHash('sha1').update(commands_json).digest('hex');

							// Make the new device type
							console.log('Device Type has been created', 'DeviceId=' + device_id, 'DeviceTypeId=NEW', 'DeviceTypeName=' + type_name);
							db.query('INSERT INTO DeviceType(name, baseName, commands) VALUES(?, ?, ?);', [type_name, base_type_name, commands_json],
							function (err, result) {

								if (err) {
									console.warn('Could not make the device type', err.toString());
									return;
								}

								type_id = result.insertId;

								// Update the device type ID and commands of the device
								db.query('UPDATE Device SET deviceTypeId = ?, commands = ? WHERE id = ?;', [type_id, commands_json, device_id],
								function (err, result) {

									if (err) console.warn(err.toString());

								});

							});

						}

					});

				});

			});

		});

	}

};
<|endoftext|>


<commit_before>const temp = require('temp')
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const GitHubApi = require('github')
const request = require('request')
const rootPackageJson = require('../package.json')

if (!process.env.ELECTRON_NPM_OTP) {
  console.error('Please set ELECTRON_NPM_OTP')
  process.exit(1)
}

const github = new GitHubApi({
  // debug: true,
  headers: { 'User-Agent': 'electron-npm-publisher' },
  followRedirects: false
})

let tempDir
temp.track()   // track and cleanup files at exit

const files = [
  'cli.js',
  'index.js',
  'install.js',
  'package.json',
  'README.md',
  'LICENSE'
]

const jsonFields = [
  'name',
  'version',
  'repository',
  'description',
  'license',
  'author',
  'keywords'
]

let npmTag = ''

new Promise((resolve, reject) => {
  temp.mkdir('electron-npm', (err, dirPath) => {
    if (err) {
      reject(err)
    } else {
      resolve(dirPath)
    }
  })
})
.then((dirPath) => {
  tempDir = dirPath
  // copy files from `/npm` to temp directory
  files.forEach((name) => {
    const noThirdSegment = name === 'README.md' || name === 'LICENSE'
    fs.writeFileSync(
      path.join(tempDir, name),
      fs.readFileSync(path.join(__dirname, '..', noThirdSegment ? '' : 'npm', name))
    )
  })
  // copy from root package.json to temp/package.json
  const packageJson = require(path.join(tempDir, 'package.json'))
  jsonFields.forEach((fieldName) => {
    packageJson[fieldName] = rootPackageJson[fieldName]
  })
  fs.writeFileSync(
    path.join(tempDir, 'package.json'),
    JSON.stringify(packageJson, null, 2)
  )

  return github.repos.getReleases({
    owner: 'electron',
    repo: rootPackageJson.version.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
  })
})
.then((releases) => {
  // download electron.d.ts from release
  const release = releases.data.find(
    (release) => release.tag_name === `v${rootPackageJson.version}`
  )
  if (!release) {
    throw new Error(`cannot find release with tag v${rootPackageJson.version}`)
  }
  return release
})
.then((release) => {
  const tsdAsset = release.assets.find((asset) => asset.name === 'electron.d.ts')
  if (!tsdAsset) {
    throw new Error(`cannot find electron.d.ts from v${rootPackageJson.version} release assets`)
  }
  return new Promise((resolve, reject) => {
    request.get({
      url: tsdAsset.url,
      headers: {
        'accept': 'application/octet-stream',
        'user-agent': 'electron-npm-publisher'
      }
    }, (err, response, body) => {
      if (err || response.statusCode !== 200) {
        reject(err || new Error('Cannot download electron.d.ts'))
      } else {
        fs.writeFileSync(path.join(tempDir, 'electron.d.ts'), body)
        resolve(release)
      }
    })
  })
})
.then((release) => {
  if (release.tag_name.indexOf('nightly') > 0) {
    npmTag = 'nightly'
  } else {
    npmTag = release.prerelease ? 'beta' : 'latest'
  }
})
.then(() => childProcess.execSync('npm pack', { cwd: tempDir }))
.then(() => {
  // test that the package can install electron prebuilt from github release
  const tarballPath = path.join(tempDir, `${rootPackageJson.name}-${rootPackageJson.version}.tgz`)
  return new Promise((resolve, reject) => {
    childProcess.execSync(`npm install ${tarballPath} --force --silent`, {
      env: Object.assign({}, process.env, { electron_config_cache: tempDir }),
      cwd: tempDir
    })
    resolve(tarballPath)
  })
})
.then((tarballPath) => childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`))
.catch((err) => {
  console.error(`Error: ${err}`)
  process.exit(1)
})
<commit_msg>fix: add dist tags for release branches (#14186)<commit_after>const temp = require('temp')
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const GitHubApi = require('github')
const {GitProcess} = require('dugite')
const request = require('request')
const rootPackageJson = require('../package.json')

if (!process.env.ELECTRON_NPM_OTP) {
  console.error('Please set ELECTRON_NPM_OTP')
  process.exit(1)
}

const github = new GitHubApi({
  // debug: true,
  headers: { 'User-Agent': 'electron-npm-publisher' },
  followRedirects: false
})

let tempDir
temp.track()   // track and cleanup files at exit

const files = [
  'cli.js',
  'index.js',
  'install.js',
  'package.json',
  'README.md',
  'LICENSE'
]

const jsonFields = [
  'name',
  'version',
  'repository',
  'description',
  'license',
  'author',
  'keywords'
]

let npmTag = ''

new Promise((resolve, reject) => {
  temp.mkdir('electron-npm', (err, dirPath) => {
    if (err) {
      reject(err)
    } else {
      resolve(dirPath)
    }
  })
})
.then((dirPath) => {
  tempDir = dirPath
  // copy files from `/npm` to temp directory
  files.forEach((name) => {
    const noThirdSegment = name === 'README.md' || name === 'LICENSE'
    fs.writeFileSync(
      path.join(tempDir, name),
      fs.readFileSync(path.join(__dirname, '..', noThirdSegment ? '' : 'npm', name))
    )
  })
  // copy from root package.json to temp/package.json
  const packageJson = require(path.join(tempDir, 'package.json'))
  jsonFields.forEach((fieldName) => {
    packageJson[fieldName] = rootPackageJson[fieldName]
  })
  fs.writeFileSync(
    path.join(tempDir, 'package.json'),
    JSON.stringify(packageJson, null, 2)
  )

  return github.repos.getReleases({
    owner: 'electron',
    repo: rootPackageJson.version.indexOf('nightly') > 0 ? 'nightlies' : 'electron'
  })
})
.then((releases) => {
  // download electron.d.ts from release
  const release = releases.data.find(
    (release) => release.tag_name === `v${rootPackageJson.version}`
  )
  if (!release) {
    throw new Error(`cannot find release with tag v${rootPackageJson.version}`)
  }
  return release
})
.then((release) => {
  const tsdAsset = release.assets.find((asset) => asset.name === 'electron.d.ts')
  if (!tsdAsset) {
    throw new Error(`cannot find electron.d.ts from v${rootPackageJson.version} release assets`)
  }
  return new Promise((resolve, reject) => {
    request.get({
      url: tsdAsset.url,
      headers: {
        'accept': 'application/octet-stream',
        'user-agent': 'electron-npm-publisher'
      }
    }, (err, response, body) => {
      if (err || response.statusCode !== 200) {
        reject(err || new Error('Cannot download electron.d.ts'))
      } else {
        fs.writeFileSync(path.join(tempDir, 'electron.d.ts'), body)
        resolve(release)
      }
    })
  })
})
.then(async (release) => {
  if (release.tag_name.indexOf('nightly') > 0) {
    const currentBranch = await getCurrentBranch()
    if (currentBranch === 'master') {
      npmTag = 'nightly'
    } else {
      npmTag = `nightly-${currentBranch}`
    }
  } else {
    npmTag = release.prerelease ? 'beta' : 'latest'
  }
})
.then(() => childProcess.execSync('npm pack', { cwd: tempDir }))
.then(() => {
  // test that the package can install electron prebuilt from github release
  const tarballPath = path.join(tempDir, `${rootPackageJson.name}-${rootPackageJson.version}.tgz`)
  return new Promise((resolve, reject) => {
    childProcess.execSync(`npm install ${tarballPath} --force --silent`, {
      env: Object.assign({}, process.env, { electron_config_cache: tempDir }),
      cwd: tempDir
    })
    resolve(tarballPath)
  })
})
.then((tarballPath) => childProcess.execSync(`npm publish ${tarballPath} --tag ${npmTag} --otp=${process.env.ELECTRON_NPM_OTP}`))
.catch((err) => {
  console.error(`Error: ${err}`)
  process.exit(1)
})

async function getCurrentBranch () {
  const gitDir = path.resolve(__dirname, '..')
  console.log(`Determining current git branch`)
  let gitArgs = ['rev-parse', '--abbrev-ref', 'HEAD']
  let branchDetails = await GitProcess.exec(gitArgs, gitDir)
  if (branchDetails.exitCode === 0) {
    let currentBranch = branchDetails.stdout.trim()
    console.log(`Successfully determined current git branch is ` +
      `${currentBranch}`)
    return currentBranch
  } else {
    let error = GitProcess.parseError(branchDetails.stderr)
    console.log(`Could not get details for the current branch,
      error was ${branchDetails.stderr}`, error)
    process.exit(1)
  }
}
<|endoftext|>


<commit_before>const {resolve} = require('path');
const webpack = require('webpack');

const ALIASES = require('ocular-dev-tools/config/ocular.config')({
  aliasMode: 'src',
  root: resolve(__dirname, '..')
}).aliases;

const PACKAGE_ROOT = resolve('.');
const PACKAGE_INFO = require(resolve(PACKAGE_ROOT, 'package.json'));

/**
 * peerDependencies are excluded using `externals`
 * https://webpack.js.org/configuration/externals/
 * e.g. @deck.gl/core is not bundled with @deck.gl/geo-layers
 */
function getExternals(packageInfo) {
  let externals = {
    // Hard coded externals
    'h3-js': 'h3',
    's2-geometry': 'S2'
  };
  const {peerDependencies = {}} = packageInfo;

  for (const depName in peerDependencies) {
    if (depName.startsWith('@deck.gl')) {
      // Instead of bundling the dependency, import from the global `deck` object
      externals[depName] = 'deck';
    }
  }

  if (externals['@deck.gl/core']) {
    // Do not bundle luma.gl if `core` is peer dependency
    externals = [
      externals,
      (context, request, callback) => {
        if (/^@?luma\.gl/.test(request)) {
          return callback(null, 'luma');
        }
        return callback();
      }
    ];
  }

  return externals;
}

const config = {
  mode: 'production',

  entry: {
    main: resolve('./bundle')
  },

  output: {
    libraryTarget: 'umd',
    path: PACKAGE_ROOT,
    filename: 'dist.min.js'
  },

  resolve: {
    alias: ALIASES
  },

  module: {
    rules: [
      {
        // Compile ES2015 using babel
        test: /\.js$/,
        loader: 'babel-loader',
        include: /src/,
        options: {
          presets: [['@babel/preset-env', {forceAllTransforms: true}]],
          // all of the helpers will reference the module @babel/runtime to avoid duplication
          // across the compiled output.
          plugins: ['@babel/transform-runtime']
        }
      }
    ]
  },

  externals: getExternals(PACKAGE_INFO),

  plugins: [
    // This is used to define the __VERSION__ constant in core/lib/init.js
    // babel-plugin-version-inline uses the package version from the working directory
    // Therefore we need to manually import the correct version from the core
    // This is called in prepublishOnly, after lerna bumps the package versions
    new webpack.DefinePlugin({
      __VERSION__: JSON.stringify(PACKAGE_INFO.version)
    })
    // Uncomment for bundle size debug
    // ,new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)()
  ],

  node: {
    Buffer: false
  },

  devtool: false
};

module.exports = (env = {}) => {
  // console.log(JSON.stringify(env, null, 2));

  if (env.dev) {
    // Set development mode (no minification)
    config.mode = 'development';
    // Remove .min from the name
    config.output.filename = 'dist/dist.dev.js';
    // Disable transpilation
    config.module.rules = [];
  }

  // NOTE uncomment to display config
  // console.log('webpack config', JSON.stringify(config, null, 2));

  return config;
};
<commit_msg>update bundle configs (#3264)<commit_after>const {resolve} = require('path');
const webpack = require('webpack');

const ALIASES = require('ocular-dev-tools/config/ocular.config')({
  aliasMode: 'src',
  root: resolve(__dirname, '..')
}).aliases;

const PACKAGE_ROOT = resolve('.');
const PACKAGE_INFO = require(resolve(PACKAGE_ROOT, 'package.json'));

/**
 * peerDependencies are excluded using `externals`
 * https://webpack.js.org/configuration/externals/
 * e.g. @deck.gl/core is not bundled with @deck.gl/geo-layers
 */
function getExternals(packageInfo) {
  let externals = {
    // Hard coded externals
    'h3-js': 'h3',
    's2-geometry': 'S2'
  };
  const {peerDependencies = {}} = packageInfo;

  for (const depName in peerDependencies) {
    if (depName.startsWith('@deck.gl')) {
      // Instead of bundling the dependency, import from the global `deck` object
      externals[depName] = 'deck';
    }
  }

  if (externals['@deck.gl/core']) {
    // Do not bundle luma.gl if `core` is peer dependency
    externals = [
      externals,
      (context, request, callback) => {
        if (/^@?luma\.gl/.test(request)) {
          return callback(null, 'luma');
        }
        return callback();
      }
    ];
  }

  return externals;
}

const config = {
  mode: 'production',

  entry: {
    main: resolve('./bundle')
  },

  output: {
    libraryTarget: 'umd',
    path: PACKAGE_ROOT,
    filename: 'dist.min.js'
  },

  resolve: {
    alias: ALIASES
  },

  module: {
    rules: [
      {
        // Compile ES2015 using babel
        test: /\.js$/,
        loader: 'babel-loader',
        include: /src/,
        options: {
          presets: [['@babel/preset-env', {forceAllTransforms: true}]],
          // all of the helpers will reference the module @babel/runtime to avoid duplication
          // across the compiled output.
          plugins: [
            '@babel/transform-runtime',
            'inline-webgl-constants',
            ['remove-glsl-comments', {patterns: ['**/*.glsl.js']}]
          ]
        }
      }
    ]
  },

  externals: getExternals(PACKAGE_INFO),

  plugins: [
    // This is used to define the __VERSION__ constant in core/lib/init.js
    // babel-plugin-version-inline uses the package version from the working directory
    // Therefore we need to manually import the correct version from the core
    // This is called in prepublishOnly, after lerna bumps the package versions
    new webpack.DefinePlugin({
      __VERSION__: JSON.stringify(PACKAGE_INFO.version)
    })
    // Uncomment for bundle size debug
    // ,new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)()
  ],

  node: {
    Buffer: false
  },

  devtool: false
};

module.exports = (env = {}) => {
  // console.log(JSON.stringify(env, null, 2));

  if (env.dev) {
    // Set development mode (no minification)
    config.mode = 'development';
    // Remove .min from the name
    config.output.filename = 'dist/dist.dev.js';
    // Disable transpilation
    config.module.rules = [];
  }

  // NOTE uncomment to display config
  // console.log('webpack config', JSON.stringify(config, null, 2));

  return config;
};
<|endoftext|>


<commit_before>'use strict';

const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const merge = require('webpack-merge');
const makeWebpackConfig = require('./make-webpack-config');

module.exports = function createServer(config, env) {
	const webpackConfig = makeWebpackConfig(config, env);
	const webpackDevServerConfig = merge(webpackConfig.devServer, {
		noInfo: true,
		compress: true,
		clientLogLevel: 'none',
		hot: true,
		quiet: true,
		watchOptions: {
			ignored: /node_modules/,
		},
		contentBase: config.assetsDir,
		watchContentBase: config.assetsDir !== undefined,
		stats: webpackConfig.stats || {},
	});

	const compiler = webpack(webpackConfig);
	const devServer = new WebpackDevServer(compiler, webpackDevServerConfig);

	// User defined customizations
	if (config.configureServer) {
		config.configureServer(devServer.app, env);
	}

	return { app: devServer, compiler };
};
<commit_msg>Fix: Allow overriding default webpack dev server options (#690)<commit_after>'use strict';

const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const merge = require('webpack-merge');
const makeWebpackConfig = require('./make-webpack-config');

module.exports = function createServer(config, env) {
	const webpackConfig = makeWebpackConfig(config, env);
	const webpackDevServerConfig = merge({
		noInfo: true,
		compress: true,
		clientLogLevel: 'none',
		hot: true,
		quiet: true,
		watchOptions: {
			ignored: /node_modules/,
		},
		watchContentBase: config.assetsDir !== undefined,
		stats: webpackConfig.stats || {},
	}, webpackConfig.devServer, {
		contentBase: config.assetsDir
	});

	const compiler = webpack(webpackConfig);
	const devServer = new WebpackDevServer(compiler, webpackDevServerConfig);

	// User defined customizations
	if (config.configureServer) {
		config.configureServer(devServer.app, env);
	}

	return { app: devServer, compiler };
};
<|endoftext|>


<commit_before>$(document).ready(
	function() {
		var IMPORT_EDITOR_SAVE_TIMEOUT = 2000;

		var import_editor = ace.edit('import-editor');
		import_editor.setTheme('ace/theme/twilight');
		import_editor.setShowInvisibles(true);
		import_editor.setShowPrintMargin(false);

		var FormatPoints = function(points, cursor_position) {
			points = points.map(
				function(point, index) {
					if (
						typeof cursor_position == 'undefined'
						|| cursor_position.row != index
					) {
						return point.replace(/\s+$/, '');
					} else {
						return point;
					}
				}
			);

			while (
				points.length
				&& points[0].trim().length == 0
				&& (typeof cursor_position == 'undefined'
				|| cursor_position.row > 0)
			) {
				points.shift();
				if (typeof cursor_position != 'undefined') {
					cursor_position.row--;
				}
			}
			while (
				points.length
				&& points.slice(-1)[0].trim().length == 0
				&& (typeof cursor_position == 'undefined'
				|| cursor_position.row + 1 < points.length)
			) {
				points.pop();
			}
			if (points.length && points.slice(-1)[0].length != 0) {
				points.push('');
			}

			if (typeof cursor_position != 'undefined') {
				return {
					points: points,
					cursor_position: cursor_position
				};
			} else {
				return points;
			}
		};
		var FormatPointsDescription = function(
			points_description,
			cursor_position
		) {
			var points = points_description.split('\n');
			var result = FormatPoints(points, cursor_position);
			points_description = result.points.join('\n');

			return {
				points_description: points_description,
				cursor_position: result.cursor_position
			};
		};
		import_editor.formatAndReturnPointsDescription = function() {
			var points_description = import_editor.getValue();
			var cursor_position = import_editor.getCursorPosition();
			var result = FormatPointsDescription(
				points_description,
				cursor_position
			);

			import_editor.setValue(result.points_description, -1);
			if (typeof result.cursor_position != 'undefined') {
				import_editor.moveCursorToPosition(result.cursor_position);
			}

			return result.points_description;
		};

		var saved_flag_container = $('.saved-flag');
		var saved_flag_icon = $('span', saved_flag_container);
		var SetSavedFlag = function(saved) {
			if (saved) {
				saved_flag_container
					.addClass('label-success')
					.removeClass('label-danger');
				saved_flag_icon
					.addClass('glyphicon-floppy-saved')
					.removeClass('glyphicon-floppy-remove');
			} else {
				saved_flag_container
					.addClass('label-danger')
					.removeClass('label-success');
				saved_flag_icon
					.addClass('glyphicon-floppy-remove')
					.removeClass('glyphicon-floppy-saved');
			}
		};

		var number_of_points_view = $('.number-of-points-view');
		var FormatNumberOfPoints = function(number_of_points) {
			var unit = '';
			var modulo = number_of_points % 10;
			if (
				modulo == 1
				&& (number_of_points < 10 || number_of_points > 20)
			) {
				unit = 'пункт';
			} else if (
				modulo > 1 && modulo < 5
				&& (number_of_points < 10 || number_of_points > 20)
			) {
				unit = 'пункта';
			} else {
				unit = 'пунктов';
			}

			return number_of_points.toString() + ' ' + unit;
		};
		var SetNumberOfPoints = function() {
			var points_description = import_editor.getValue();
			var points = points_description.split('\n');
			points = FormatPoints(points);

			var number_of_points = points.length - 1;
			if (number_of_points < 0) {
				number_of_points = 0;
			}

			var formatted_number_of_points = FormatNumberOfPoints(
				number_of_points
			);
			number_of_points_view.text(formatted_number_of_points);
		};

		import_editor.on(
			'change',
			function() {
				SetSavedFlag(false);
				SetNumberOfPoints();
			}
		);
		import_editor.on(
			'paste',
			function(event) {
				event.text =
					event.text
					.replace(/\u21e5|\u00b7/g, ' ')
					.replace('\u00b6', '');
			}
		);

		var save_button = $('.save-import-button');
		var save_url = save_button.data('save-url');
		var processing_animation_image = $('img', save_button);
		var save_icon = $('span', save_button);
		var import_editor_container = $(import_editor.container);
		var FinishAnimation = function() {
			save_button.prop('disabled', false);
			processing_animation_image.hide();
			save_icon.show();
			import_editor_container.removeClass('wait');
		};
		var SaveViaAjax = function() {
			save_button.prop('disabled', true);
			processing_animation_image.show();
			save_icon.hide();
			import_editor_container.addClass('wait');

			var data = $.extend(
				{
					'Import[points_description]':
						import_editor
						.formatAndReturnPointsDescription()
				},
				CSRF_TOKEN
			);
			$.post(
				save_url,
				data,
				function() {
					SetSavedFlag(true);
					FinishAnimation();
				}
			).fail(
				function(xhr, text_status) {
					FinishAnimation();
					AjaxErrorDialog.handler(xhr, text_status);
				}
			);
		};

		var form = $('.import-form');
		form.submit(
			function() {
				var points_description =
					import_editor
					.formatAndReturnPointsDescription();
				$('#Import_points_description').val(points_description);
			}
		);

		save_button.click(SaveViaAjax);

		var save_and_import_button = $('.save-and-import-button');
		var import_date = save_and_import_button.data('date');
		var import_my_date = save_and_import_button.data('my-date');
		save_and_import_button.click(
			function() {
				ImportDialog.show(
					import_my_date,
					import_date,
					function() {
						$('#Import_import').val('true');
						form.submit();
					}
				);
			}
		);

		$(window).keydown(
			function(event) {
				if (
					(event.ctrlKey || event.metaKey)
					&& String.fromCharCode(event.which).toLowerCase() == 's'
				) {
					event.preventDefault();
					SaveViaAjax();
				}
			}
		);
	}
);
<commit_msg>Fixes #75: add additional flag of saving state of import editor.<commit_after>$(document).ready(
	function() {
		var IMPORT_EDITOR_SAVE_TIMEOUT = 2000;

		var import_editor = ace.edit('import-editor');
		import_editor.setTheme('ace/theme/twilight');
		import_editor.setShowInvisibles(true);
		import_editor.setShowPrintMargin(false);

		var FormatPoints = function(points, cursor_position) {
			points = points.map(
				function(point, index) {
					if (
						typeof cursor_position == 'undefined'
						|| cursor_position.row != index
					) {
						return point.replace(/\s+$/, '');
					} else {
						return point;
					}
				}
			);

			while (
				points.length
				&& points[0].trim().length == 0
				&& (typeof cursor_position == 'undefined'
				|| cursor_position.row > 0)
			) {
				points.shift();
				if (typeof cursor_position != 'undefined') {
					cursor_position.row--;
				}
			}
			while (
				points.length
				&& points.slice(-1)[0].trim().length == 0
				&& (typeof cursor_position == 'undefined'
				|| cursor_position.row + 1 < points.length)
			) {
				points.pop();
			}
			if (points.length && points.slice(-1)[0].length != 0) {
				points.push('');
			}

			if (typeof cursor_position != 'undefined') {
				return {
					points: points,
					cursor_position: cursor_position
				};
			} else {
				return points;
			}
		};
		var FormatPointsDescription = function(
			points_description,
			cursor_position
		) {
			var points = points_description.split('\n');
			var result = FormatPoints(points, cursor_position);
			points_description = result.points.join('\n');

			return {
				points_description: points_description,
				cursor_position: result.cursor_position
			};
		};
		import_editor.formatAndReturnPointsDescription = function() {
			var points_description = import_editor.getValue();
			var cursor_position = import_editor.getCursorPosition();
			var result = FormatPointsDescription(
				points_description,
				cursor_position
			);

			import_editor.setValue(result.points_description, -1);
			if (typeof result.cursor_position != 'undefined') {
				import_editor.moveCursorToPosition(result.cursor_position);
			}

			return result.points_description;
		};

		var saved_flag_container = $('.saved-flag');
		var saved_flag_icon = $('span', saved_flag_container);
		var SetSavedFlag = function(saved) {
			if (saved) {
				saved_flag_container
					.addClass('label-success')
					.removeClass('label-danger');
				saved_flag_icon
					.addClass('glyphicon-floppy-saved')
					.removeClass('glyphicon-floppy-remove');

				var last_symbol = document.title.slice(-1);
				if (last_symbol.length && last_symbol == '*') {
					document.title = document.title.slice(0, -1);
				}
			} else {
				saved_flag_container
					.addClass('label-danger')
					.removeClass('label-success');
				saved_flag_icon
					.addClass('glyphicon-floppy-remove')
					.removeClass('glyphicon-floppy-saved');

				var last_symbol = document.title.slice(-1);
				if (last_symbol.length && last_symbol != '*') {
					document.title += '*';
				}
			}
		};

		var number_of_points_view = $('.number-of-points-view');
		var FormatNumberOfPoints = function(number_of_points) {
			var unit = '';
			var modulo = number_of_points % 10;
			if (
				modulo == 1
				&& (number_of_points < 10 || number_of_points > 20)
			) {
				unit = 'пункт';
			} else if (
				modulo > 1 && modulo < 5
				&& (number_of_points < 10 || number_of_points > 20)
			) {
				unit = 'пункта';
			} else {
				unit = 'пунктов';
			}

			return number_of_points.toString() + ' ' + unit;
		};
		var SetNumberOfPoints = function() {
			var points_description = import_editor.getValue();
			var points = points_description.split('\n');
			points = FormatPoints(points);

			var number_of_points = points.length - 1;
			if (number_of_points < 0) {
				number_of_points = 0;
			}

			var formatted_number_of_points = FormatNumberOfPoints(
				number_of_points
			);
			number_of_points_view.text(formatted_number_of_points);
		};

		import_editor.on(
			'change',
			function() {
				SetSavedFlag(false);
				SetNumberOfPoints();
			}
		);
		import_editor.on(
			'paste',
			function(event) {
				event.text =
					event.text
					.replace(/\u21e5|\u00b7/g, ' ')
					.replace('\u00b6', '');
			}
		);

		var save_button = $('.save-import-button');
		var save_url = save_button.data('save-url');
		var processing_animation_image = $('img', save_button);
		var save_icon = $('span', save_button);
		var import_editor_container = $(import_editor.container);
		var FinishAnimation = function() {
			save_button.prop('disabled', false);
			processing_animation_image.hide();
			save_icon.show();
			import_editor_container.removeClass('wait');
		};
		var SaveViaAjax = function() {
			save_button.prop('disabled', true);
			processing_animation_image.show();
			save_icon.hide();
			import_editor_container.addClass('wait');

			var data = $.extend(
				{
					'Import[points_description]':
						import_editor
						.formatAndReturnPointsDescription()
				},
				CSRF_TOKEN
			);
			$.post(
				save_url,
				data,
				function() {
					SetSavedFlag(true);
					FinishAnimation();
				}
			).fail(
				function(xhr, text_status) {
					FinishAnimation();
					AjaxErrorDialog.handler(xhr, text_status);
				}
			);
		};

		var form = $('.import-form');
		form.submit(
			function() {
				var points_description =
					import_editor
					.formatAndReturnPointsDescription();
				$('#Import_points_description').val(points_description);
			}
		);

		save_button.click(SaveViaAjax);

		var save_and_import_button = $('.save-and-import-button');
		var import_date = save_and_import_button.data('date');
		var import_my_date = save_and_import_button.data('my-date');
		save_and_import_button.click(
			function() {
				ImportDialog.show(
					import_my_date,
					import_date,
					function() {
						$('#Import_import').val('true');
						form.submit();
					}
				);
			}
		);

		$(window).keydown(
			function(event) {
				if (
					(event.ctrlKey || event.metaKey)
					&& String.fromCharCode(event.which).toLowerCase() == 's'
				) {
					event.preventDefault();
					SaveViaAjax();
				}
			}
		);
	}
);
<|endoftext|>


<commit_before>/*    Copyright 2021 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const _ = require('lodash');

const validator = require('validator');

const IntelTool = require('../net2/IntelTool');
const intelTool = new IntelTool();

const rclient = require('../util/redis_manager.js').getRedisClient();

const m = require('../extension/metrics/metrics.js');

const flowUtil = require('../net2/FlowUtil');
const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsmasqConfigFolder = `${userConfigFolder}/dnsmasq`;

const cc = require('../extension/cloudcache/cloudcache.js');

const zlib = require('zlib');
const fs = require('fs');

const Promise = require('bluebird');
const inflateAsync = Promise.promisify(zlib.inflate);
Promise.promisifyAll(fs);

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

const sys = require('sys'),
      Buffer = require('buffer').Buffer,
      dgram = require('dgram');

// slices a single byte into bits
// assuming only single bytes
const sliceBits = function(b, off, len) {
    const s = 7 - (off + len - 1);

    b = b >>> s;
    return b & ~(0xff << len);
};

const qnameToDomain = (qname) => {
  let domain= '';
  for(let i=0;i<qname.length;i++) {
    if (qname[i] == 0) {
      //last char chop trailing .
      domain = domain.substring(0, domain.length - 1);
      break;
    }

    const tmpBuf = qname.slice(i+1, i+qname[i]+1);
    domain += tmpBuf.toString('binary', 0, tmpBuf.length);
    domain += '.';

    i = i + qname[i];
  }

  return domain;
};

const defaultExpireTime = 48 * 3600; // expire in two days, by default
const allowKey = "dns_proxy:allow_list"; //unused at this moment
const passthroughKey = "dns_proxy:passthrough_list";
const blockKey = "dns_proxy:block_list";
const featureName = "dns_proxy";
const defaultListenPort = 9963;

class DNSProxyPlugin extends Sensor {
  async run() {
    // invalidate cache keys when starting up
    await rclient.delAsync(allowKey);
    await rclient.delAsync(blockKey);
    await rclient.delAsync(passthroughKey);

    this.hookFeature(featureName);
  }

  getHashKeyName(item = {}) {
    if(!item.count || !item.error || !item.prefix) {
      log.error("Invalid item:", item);
      return null;
    }

    const {count, error, prefix} = item;

    return `bf:${prefix}:${count}:${error}`;
  }

  getFilePath(item = {}) {
    if(!item.count || !item.error || !item.prefix) {
      log.error("Invalid item:", item);
      return null;
    }

    const {count, error, prefix} = item;

    return `${f.getRuntimeInfoFolder()}/${featureName}.${prefix}.bf.data`;
  }

  getDnsmasqConfigFile() {
    return `${dnsmasqConfigFolder}/${featureName}.conf`;
  }

  async enableDnsmasqConfig() {
    const data = this.config.data || [];

    let dnsmasqEntry = "mac-address-tag=%FF:FF:FF:FF:FF:FF$dns_proxy\n";

    for(const item of data) {
      const fp = this.getFilePath(item);
      if(!fp) {
        continue;
      }

      const entry = `server-bf-high=<${fp},${item.count},${item.error}><${allowKey}><${blockKey}><${passthroughKey}>127.0.0.1#${this.getPort()}$dns_proxy\n`;

      dnsmasqEntry += entry;
    }

    await fs.writeFileAsync(this.getDnsmasqConfigFile(), dnsmasqEntry);
    await dnsmasq.scheduleRestartDNSService();
  }

  async disableDnsmasqConfig() {
    await fs.unlinkAsync(this.getDnsmasqConfigFile());
    await dnsmasq.scheduleRestartDNSService();
  }

  async globalOn() {
    this.launchServer();
    const data = this.config.data || [];
    if(_.isEmpty(data)) {
      return;
    }

    for(const item of data) {
      const hashKeyName = this.getHashKeyName(item);
      if(!hashKeyName) continue;

      try {
        await cc.enableCache(hashKeyName, (data) => {
          this.updateBFData(item, data);
        });
      } catch(err) {
        log.error("Failed to process bf data:", item);
      }
    }

    await this.enableDnsmasqConfig();
  }

  async updateBFData(item, content) {
    try {
      if(!content || content.length < 10) {
        // likely invalid, return null for protection
        log.error(`Invalid bf data content for ${item || item.prefix}, ignored`);
        return;
      }
      const buf = Buffer.from(content, 'base64');
      const output = await inflateAsync(buf);
      const fp = this.getFilePath(item);
      if(!fp) return;

      await fs.writeFileAsync(fp, output);
    } catch(err) {
      log.error("Failed to update bf data, err:", err);
    }
  }

  async globalOff() {
    if(this.server) {
      this.server.close();
      this.server = null;
    }
    await cc.disableCache(this.getHashKeyName());
    await this.disableDnsmasqConfig();
  }

  launchServer() {
    this.server = dgram.createSocket('udp4');

    this.server.on('message', async (msg, info) => {
      const qname = this.parseRequest(msg);
      if(qname) {
        await this.processRequest(qname);
      }
      await m.incr("dns_proxy_request_cnt");
      // never need to reply back to client as this is not a true dns server
    });

    this.server.on('close', () => {
      log.info("DNS proxy server is closed.");
    });

    const port = this.getPort();
    this.server.bind(port, '127.0.0.1');
    log.info("DNS proxy server is now running at port:", port);
  }

  getPort() {
    const port = this.config.listenPort || defaultListenPort;

    if(!_.isNumber(port)) {
      log.warn("Invalid port", port, ", reverting back to default port.");
      return defaultListenPort;
    }

    return port;

  }

  parseRequest(req) {
    //see rfc1035 for more details
    //http://tools.ietf.org/html/rfc1035#section-4.1.1
    if(req.length < 16) {
      return null;
    }
    return req.slice(12, req.length - 4);
  }

  async checkCache(domain) { // only return if there is exact-match intel in redis
    return intelTool.getDomainIntel(domain);
  }

  async processRequest(qname) {
    const domain = qnameToDomain(qname);
    log.info("dns request is", domain);
    const begin = new Date() / 1;
    const cache = await this.checkCache(domain);
    if(cache) {
      log.info(`inteldns:${domain} is already cached locally, updating redis cache keys directly...`);
      await this.updateRedisCacheFromIntel(domain, cache);
      return; // do need to do anything
    }
    const result = await intelTool.checkIntelFromCloud(undefined, domain); // parameter ip is undefined since it's a dns request only
    await this.updateCache(domain, result);
    const end = new Date() / 1;
    log.info("dns intel result is", result, "took", Math.floor(end - begin), "ms");
  }

  async updateCache(domain, result) {
    if(_.isEmpty(result)) { // empty intel, means the domain is good
      const domains = flowUtil.getSubDomains(domain);
      if(!domains) {
        log.warn("Invalid Domain", domain, "skipped.");
        return;
      }

      // since result is empty, it means all sub domains of this domain are good
      const placeholder = {c: 'x', a: '1'};
      for(const dn of domains) {
        await intelTool.addDomainIntel(dn, placeholder, defaultExpireTime);
      }

      // only the exact dn is added to the passthrough and block list
      // generic domains can't be added, because another sub domain may be malicous
      // example:
      //   domain1.blogspot.com may be good
      //   and domain2.blogspot.com may be malicous
      // when domain1 is checked to be good, we should NOT add blogspot.com to passthrough_list
      await rclient.saddAsync(passthroughKey, domain);
      await rclient.sremAsync(blockKey, domain);

    } else {
      for(const item of result) {
        const dn = item.originIP; // originIP is the domain
        const isDomain = validator.isFQDN(dn);
        if(!isDomain) {
          continue;
        }

        for(const k in item) { // to suppress redis error
          const v = item[k];
          if(_.isBoolean(v) || _.isNumber(v) || _.isString(v)) {
            continue;
          }
          item[k] = JSON.stringify(v);
        }

        await intelTool.addDomainIntel(dn, item, item.e || defaultExpireTime);
        await this.updateRedisCacheFromIntel(dn, item);
      }
    }
  }

  async updateRedisCacheFromIntel(dn, item) {
    if(item.c === 'intel') { // need to decide the criteria better
      await rclient.saddAsync(blockKey, dn);
      // either passthrough or block, the same domain can't stay in both set at the same time
      await rclient.sremAsync(passthroughKey, dn);
    } else {
      await rclient.saddAsync(passthroughKey, dn);
      await rclient.sremAsync(blockKey, dn);
    }
  }
}

module.exports = DNSProxyPlugin;
<commit_msg>minor bug fix<commit_after>/*    Copyright 2021 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const _ = require('lodash');

const validator = require('validator');

const IntelTool = require('../net2/IntelTool');
const intelTool = new IntelTool();

const rclient = require('../util/redis_manager.js').getRedisClient();

const m = require('../extension/metrics/metrics.js');

const flowUtil = require('../net2/FlowUtil');
const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsmasqConfigFolder = `${userConfigFolder}/dnsmasq`;

const cc = require('../extension/cloudcache/cloudcache.js');

const zlib = require('zlib');
const fs = require('fs');

const Promise = require('bluebird');
const inflateAsync = Promise.promisify(zlib.inflate);
Promise.promisifyAll(fs);

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

const sys = require('sys'),
      Buffer = require('buffer').Buffer,
      dgram = require('dgram');

// slices a single byte into bits
// assuming only single bytes
const sliceBits = function(b, off, len) {
    const s = 7 - (off + len - 1);

    b = b >>> s;
    return b & ~(0xff << len);
};

const qnameToDomain = (qname) => {
  let domain= '';
  for(let i=0;i<qname.length;i++) {
    if (qname[i] == 0) {
      //last char chop trailing .
      domain = domain.substring(0, domain.length - 1);
      break;
    }

    const tmpBuf = qname.slice(i+1, i+qname[i]+1);
    domain += tmpBuf.toString('binary', 0, tmpBuf.length);
    domain += '.';

    i = i + qname[i];
  }

  return domain;
};

const defaultExpireTime = 48 * 3600; // expire in two days, by default
const allowKey = "dns_proxy:allow_list"; //unused at this moment
const passthroughKey = "dns_proxy:passthrough_list";
const blockKey = "dns_proxy:block_list";
const featureName = "dns_proxy";
const defaultListenPort = 9963;

class DNSProxyPlugin extends Sensor {
  async run() {
    // invalidate cache keys when starting up
    await rclient.delAsync(allowKey);
    await rclient.delAsync(blockKey);
    await rclient.delAsync(passthroughKey);

    this.hookFeature(featureName);
  }

  getHashKeyName(item = {}) {
    if(!item.count || !item.error || !item.prefix) {
      log.error("Invalid item:", item);
      return null;
    }

    const {count, error, prefix} = item;

    return `bf:${prefix}:${count}:${error}`;
  }

  getFilePath(item = {}) {
    if(!item.count || !item.error || !item.prefix) {
      log.error("Invalid item:", item);
      return null;
    }

    const {count, error, prefix} = item;

    return `${f.getRuntimeInfoFolder()}/${featureName}.${prefix}.bf.data`;
  }

  getDnsmasqConfigFile() {
    return `${dnsmasqConfigFolder}/${featureName}.conf`;
  }

  async enableDnsmasqConfig() {
    const data = this.config.data || [];

    let dnsmasqEntry = "mac-address-tag=%FF:FF:FF:FF:FF:FF$dns_proxy\n";

    for(const item of data) {
      const fp = this.getFilePath(item);
      if(!fp) {
        continue;
      }

      const entry = `server-bf-high=<${fp},${item.count},${item.error}><${allowKey}><${blockKey}><${passthroughKey}>127.0.0.1#${this.getPort()}$dns_proxy\n`;

      dnsmasqEntry += entry;
    }

    await fs.writeFileAsync(this.getDnsmasqConfigFile(), dnsmasqEntry);
    await dnsmasq.scheduleRestartDNSService();
  }

  async disableDnsmasqConfig() {
    await fs.unlinkAsync(this.getDnsmasqConfigFile());
    await dnsmasq.scheduleRestartDNSService();
  }

  async globalOn() {
    this.launchServer();
    const data = this.config.data || [];
    if(_.isEmpty(data)) {
      return;
    }

    for(const item of data) {
      const hashKeyName = this.getHashKeyName(item);
      if(!hashKeyName) continue;

      try {
        await cc.enableCache(hashKeyName, (data) => {
          this.updateBFData(item, data);
        });
      } catch(err) {
        log.error("Failed to process bf data:", item);
      }
    }

    await this.enableDnsmasqConfig();
  }

  async updateBFData(item, content) {
    try {
      if(!content || content.length < 10) {
        // likely invalid, return null for protection
        log.error(`Invalid bf data content for ${item && item.prefix}, ignored`);
        return;
      }
      const buf = Buffer.from(content, 'base64');
      const output = await inflateAsync(buf);
      const fp = this.getFilePath(item);
      if(!fp) return;

      await fs.writeFileAsync(fp, output);
    } catch(err) {
      log.error("Failed to update bf data, err:", err);
    }
  }

  async globalOff() {
    if(this.server) {
      this.server.close();
      this.server = null;
    }
    await cc.disableCache(this.getHashKeyName());
    await this.disableDnsmasqConfig();
  }

  launchServer() {
    this.server = dgram.createSocket('udp4');

    this.server.on('message', async (msg, info) => {
      const qname = this.parseRequest(msg);
      if(qname) {
        await this.processRequest(qname);
      }
      await m.incr("dns_proxy_request_cnt");
      // never need to reply back to client as this is not a true dns server
    });

    this.server.on('close', () => {
      log.info("DNS proxy server is closed.");
    });

    const port = this.getPort();
    this.server.bind(port, '127.0.0.1');
    log.info("DNS proxy server is now running at port:", port);
  }

  getPort() {
    const port = this.config.listenPort || defaultListenPort;

    if(!_.isNumber(port)) {
      log.warn("Invalid port", port, ", reverting back to default port.");
      return defaultListenPort;
    }

    return port;

  }

  parseRequest(req) {
    //see rfc1035 for more details
    //http://tools.ietf.org/html/rfc1035#section-4.1.1
    if(req.length < 16) {
      return null;
    }
    return req.slice(12, req.length - 4);
  }

  async checkCache(domain) { // only return if there is exact-match intel in redis
    return intelTool.getDomainIntel(domain);
  }

  async processRequest(qname) {
    const domain = qnameToDomain(qname);
    log.info("dns request is", domain);
    const begin = new Date() / 1;
    const cache = await this.checkCache(domain);
    if(cache) {
      log.info(`inteldns:${domain} is already cached locally, updating redis cache keys directly...`);
      await this.updateRedisCacheFromIntel(domain, cache);
      return; // do need to do anything
    }
    const result = await intelTool.checkIntelFromCloud(undefined, domain); // parameter ip is undefined since it's a dns request only
    await this.updateCache(domain, result);
    const end = new Date() / 1;
    log.info("dns intel result is", result, "took", Math.floor(end - begin), "ms");
  }

  async updateCache(domain, result) {
    if(_.isEmpty(result)) { // empty intel, means the domain is good
      const domains = flowUtil.getSubDomains(domain);
      if(!domains) {
        log.warn("Invalid Domain", domain, "skipped.");
        return;
      }

      // since result is empty, it means all sub domains of this domain are good
      const placeholder = {c: 'x', a: '1'};
      for(const dn of domains) {
        await intelTool.addDomainIntel(dn, placeholder, defaultExpireTime);
      }

      // only the exact dn is added to the passthrough and block list
      // generic domains can't be added, because another sub domain may be malicous
      // example:
      //   domain1.blogspot.com may be good
      //   and domain2.blogspot.com may be malicous
      // when domain1 is checked to be good, we should NOT add blogspot.com to passthrough_list
      await rclient.saddAsync(passthroughKey, domain);
      await rclient.sremAsync(blockKey, domain);

    } else {
      for(const item of result) {
        const dn = item.originIP; // originIP is the domain
        const isDomain = validator.isFQDN(dn);
        if(!isDomain) {
          continue;
        }

        for(const k in item) { // to suppress redis error
          const v = item[k];
          if(_.isBoolean(v) || _.isNumber(v) || _.isString(v)) {
            continue;
          }
          item[k] = JSON.stringify(v);
        }

        await intelTool.addDomainIntel(dn, item, item.e || defaultExpireTime);
        await this.updateRedisCacheFromIntel(dn, item);
      }
    }
  }

  async updateRedisCacheFromIntel(dn, item) {
    if(item.c === 'intel') { // need to decide the criteria better
      await rclient.saddAsync(blockKey, dn);
      // either passthrough or block, the same domain can't stay in both set at the same time
      await rclient.sremAsync(passthroughKey, dn);
    } else {
      await rclient.saddAsync(passthroughKey, dn);
      await rclient.sremAsync(blockKey, dn);
    }
  }
}

module.exports = DNSProxyPlugin;
<|endoftext|>


<commit_before>var R = require('ramda'),
  Rx = require('rx'),
  assign = require('object.assign'),
  debug = require('debug')('freecc:challenges'),
  utils = require('../utils'),

  // this would be so much cleaner with destructering...
  saveUser = require('../utils/rx').saveUser,
  observableQueryFromModel = require('../utils/rx').observableQueryFromModel,

  userMigration = require('../utils/middleware').userMigration,
  ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo,
  ifNoUserSend = require('../utils/middleware').ifNoUserSend;

var challengeMapWithNames = utils.getChallengeMapWithNames();
var challengeMapWithIds = utils.getChallengeMapWithIds();
var challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames();
var challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i;

var dasherize = utils.dasherize;
var unDasherize = utils.unDasherize;

var getMDNLinks = utils.getMDNLinks;

function updateUserProgress(user, challengeId, completedChallenge) {
  var alreadyCompleted = user.completedChallenges.some(({ id }) => {
    return id === challengeId;
  });

  if (alreadyCompleted) {
    user.progressTimestamps.push(Date.now());
  }
  user.completedChallenges.push(completedChallenge);
  return user;
}

module.exports = function(app) {
  var router = app.loopback.Router();
  var Challenge = app.models.Challenge;
  var User = app.models.User;
  var redirectNonUser =
    ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works');
  var send200toNonUser = ifNoUserSend(true);

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    redirectNonUser,
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  router.get(
    '/challenges/',
    redirectNonUser,
    returnCurrentChallenge
  );

  app.use(router);

  function returnNextChallenge(req, res, next) {

    // find the user's current challenge and block
    // look in that block and find the index of their current challenge
    // if index + 1 < block.challenges.length
    // serve index + 1 challenge
    // otherwise increment block key and serve the first challenge in that block
    // unless the next block is undefined, which means no next block
    var nextChallengeName;

    var challengeId = String(req.user.currentChallenge.challengeId);
    var challengeBlock = req.user.currentChallenge.challengeBlock;
    // TODO(berks) fix index call here
    var indexOfChallenge = challengeMapWithIds[challengeBlock]
      .indexOf(challengeId);

    if (indexOfChallenge + 1
      < challengeMapWithIds[challengeBlock].length) {
      nextChallengeName =
        challengeMapWithDashedNames[challengeBlock][++indexOfChallenge];
    } else if (typeof challengeMapWithIds[++challengeBlock] !== 'undefined') {
      nextChallengeName = R.head(challengeMapWithDashedNames[challengeBlock]);
    } else {
      req.flash('errors', {
        msg: 'It looks like you have finished all of our challenges.' +
        ' Great job! Now on to helping nonprofits!'
      });
      nextChallengeName = R.head(challengeMapWithDashedNames[0].challenges);
    }

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nextChallengeName);
        }
      );
  }

  function returnCurrentChallenge(req, res, next) {
    if (!req.user.currentChallenge) {
      req.user.currentChallenge = {};
      req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
      req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0];
      req.user.currentChallenge.challengeBlock = '0';
      req.user.currentChallenge.dashedName =
        challengeMapWithDashedNames['0'][0];
    }

    var nameString = req.user.currentChallenge.dashedName;

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nameString);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    var origChallengeName = req.params.challengeName;
    var unDashedName = unDasherize(origChallengeName);

    var challengeName = challangesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    debug('looking for ', challengeName);
    Challenge.findOne(
      { where: { name: { like: challengeName, options: 'i' } } },
      function(err, challenge) {
        if (err) { return next(err); }

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return res.redirect('/challenges');
        }
        // Redirect to full name if the user only entered a partial
        if (dasherize(challenge.name) !== origChallengeName) {
          debug('redirecting to fullname');
          return res.redirect('/challenges/' + dasherize(challenge.name));
        }

        if (req.user) {
          req.user.currentChallenge = {
            challengeId: challenge.id,
            challengeName: challenge.name,
            dashedName: challenge.dashedName,
            challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds)
                .map(function(key) {
                  return challengeMapWithIds[key]
                    .filter(function(elem) {
                      return elem === ('' + challenge.id);
                    })
                    .map(function() {
                      return key;
                    });
                })
            ))
          };
        }

        var commonLocals = {
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        };

        // TODO Berkeley
        var challengeView = {
          0: 'coursewares/showHTML',
          1: 'coursewares/showJS',
          2: 'coursewares/showVideo',
          3: 'coursewares/showZiplineOrBasejump',
          4: 'coursewares/showZiplineOrBasejump',
          5: 'coursewares/showBonfire'
        };

        saveUser(req.user)
          .subscribe(
            function() {},
            next,
            function() {
              var view = challengeView[challenge.challengeType];
              res.render(view, commonLocals);
            }
          );
      });
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Rx.Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // not iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Rx.Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName,
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) { debug('user save', user && user.progressTimestamps); },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Rx.Observable.just(req.user), function(user, pairedWith) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Rx.Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap(req, res, next) {
    var completedList = [];

    if (req.user) {
      completedList = req.user.completedChallenges;
    }

    var noDuplicatedChallenges = R.uniq(completedList);

    var completedChallengeList = noDuplicatedChallenges
      .map(function(challenge) {
        // backwards compatibility
        return (challenge.id || challenge._id);
      });
    var challengeList = utils.
      getChallengeMapForDisplay(completedChallengeList);

    Object.keys(challengeList).forEach(function(key) {
      challengeList[key].completed = challengeList[key]
        .challenges.filter(function(elem) {
        // backwards compatibility hack
        return completedChallengeList.indexOf(elem.id || elem._id) > -1;
      });
    });

    function numberWithCommas(x) {
      return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }

    var date1 = new Date('10/15/2014');
    var date2 = new Date();
    var timeDiff = Math.abs(date2.getTime() - date1.getTime());
    var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24));

    User.count(function(err, camperCount) {
      if (err) { return next(err); }

      res.render('challengeMap/show', {
        daysRunning: daysRunning,
        camperCount: numberWithCommas(camperCount),
        title: "A map of all Free Code Camp's Challenges",
        challengeList: challengeList,
        completedChallengeList: completedChallengeList
      });
    });
  }
};
<commit_msg>fix order of user/pairedWith in withLatestFrom operator in challenges<commit_after>var R = require('ramda'),
  Rx = require('rx'),
  assign = require('object.assign'),
  debug = require('debug')('freecc:challenges'),
  utils = require('../utils'),

  // this would be so much cleaner with destructering...
  saveUser = require('../utils/rx').saveUser,
  observableQueryFromModel = require('../utils/rx').observableQueryFromModel,

  userMigration = require('../utils/middleware').userMigration,
  ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo,
  ifNoUserSend = require('../utils/middleware').ifNoUserSend;

var challengeMapWithNames = utils.getChallengeMapWithNames();
var challengeMapWithIds = utils.getChallengeMapWithIds();
var challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames();
var challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i;

var dasherize = utils.dasherize;
var unDasherize = utils.unDasherize;

var getMDNLinks = utils.getMDNLinks;

function updateUserProgress(user, challengeId, completedChallenge) {
  var alreadyCompleted = user.completedChallenges.some(({ id }) => {
    return id === challengeId;
  });

  if (alreadyCompleted) {
    user.progressTimestamps.push(Date.now());
  }
  user.completedChallenges.push(completedChallenge);
  return user;
}

module.exports = function(app) {
  var router = app.loopback.Router();
  var Challenge = app.models.Challenge;
  var User = app.models.User;
  var redirectNonUser =
    ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works');
  var send200toNonUser = ifNoUserSend(true);

  router.post(
    '/completed-challenge/',
    send200toNonUser,
    completedChallenge
  );
  router.post(
    '/completed-zipline-or-basejump',
    send200toNonUser,
    completedZiplineOrBasejump
  );
  router.post(
    '/completed-bonfire',
    send200toNonUser,
    completedBonfire
  );

  // the follow routes are covered by userMigration
  router.use(userMigration);
  router.get('/map', challengeMap);
  router.get(
    '/challenges/next-challenge',
    redirectNonUser,
    returnNextChallenge
  );

  router.get('/challenges/:challengeName', returnIndividualChallenge);

  router.get(
    '/challenges/',
    redirectNonUser,
    returnCurrentChallenge
  );

  app.use(router);

  function returnNextChallenge(req, res, next) {

    // find the user's current challenge and block
    // look in that block and find the index of their current challenge
    // if index + 1 < block.challenges.length
    // serve index + 1 challenge
    // otherwise increment block key and serve the first challenge in that block
    // unless the next block is undefined, which means no next block
    var nextChallengeName;

    var challengeId = String(req.user.currentChallenge.challengeId);
    var challengeBlock = req.user.currentChallenge.challengeBlock;
    // TODO(berks) fix index call here
    var indexOfChallenge = challengeMapWithIds[challengeBlock]
      .indexOf(challengeId);

    if (indexOfChallenge + 1
      < challengeMapWithIds[challengeBlock].length) {
      nextChallengeName =
        challengeMapWithDashedNames[challengeBlock][++indexOfChallenge];
    } else if (typeof challengeMapWithIds[++challengeBlock] !== 'undefined') {
      nextChallengeName = R.head(challengeMapWithDashedNames[challengeBlock]);
    } else {
      req.flash('errors', {
        msg: 'It looks like you have finished all of our challenges.' +
        ' Great job! Now on to helping nonprofits!'
      });
      nextChallengeName = R.head(challengeMapWithDashedNames[0].challenges);
    }

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nextChallengeName);
        }
      );
  }

  function returnCurrentChallenge(req, res, next) {
    if (!req.user.currentChallenge) {
      req.user.currentChallenge = {};
      req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
      req.user.currentChallenge.challengeName = challengeMapWithNames['0'][0];
      req.user.currentChallenge.challengeBlock = '0';
      req.user.currentChallenge.dashedName =
        challengeMapWithDashedNames['0'][0];
    }

    var nameString = req.user.currentChallenge.dashedName;

    saveUser(req.user)
      .subscribe(
        function() {},
        next,
        function() {
          res.redirect('/challenges/' + nameString);
        }
      );
  }

  function returnIndividualChallenge(req, res, next) {
    var origChallengeName = req.params.challengeName;
    var unDashedName = unDasherize(origChallengeName);

    var challengeName = challangesRegex.test(unDashedName) ?
      // remove first word if matches
      unDashedName.split(' ').slice(1).join(' ') :
      unDashedName;

    debug('looking for ', challengeName);
    Challenge.findOne(
      { where: { name: { like: challengeName, options: 'i' } } },
      function(err, challenge) {
        if (err) { return next(err); }

        // Handle not found
        if (!challenge) {
          debug('did not find challenge for ' + origChallengeName);
          req.flash('errors', {
            msg:
              '404: We couldn\'t find a challenge with the name `' +
              origChallengeName +
              '` Please double check the name.'
          });
          return res.redirect('/challenges');
        }
        // Redirect to full name if the user only entered a partial
        if (dasherize(challenge.name) !== origChallengeName) {
          debug('redirecting to fullname');
          return res.redirect('/challenges/' + dasherize(challenge.name));
        }

        if (req.user) {
          req.user.currentChallenge = {
            challengeId: challenge.id,
            challengeName: challenge.name,
            dashedName: challenge.dashedName,
            challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds)
                .map(function(key) {
                  return challengeMapWithIds[key]
                    .filter(function(elem) {
                      return elem === ('' + challenge.id);
                    })
                    .map(function() {
                      return key;
                    });
                })
            ))
          };
        }

        var commonLocals = {
          title: challenge.name,
          dashedName: origChallengeName,
          name: challenge.name,
          details: challenge.description,
          tests: challenge.tests,
          challengeSeed: challenge.challengeSeed,
          verb: utils.randomVerb(),
          phrase: utils.randomPhrase(),
          compliment: utils.randomCompliment(),
          challengeId: challenge.id,
          challengeType: challenge.challengeType,
          // video challenges
          video: challenge.challengeSeed[0],
          // bonfires specific
          difficulty: Math.floor(+challenge.difficulty),
          bonfires: challenge,
          MDNkeys: challenge.MDNlinks,
          MDNlinks: getMDNLinks(challenge.MDNlinks),
          // htmls specific
          environment: utils.whichEnvironment()
        };

        // TODO Berkeley
        var challengeView = {
          0: 'coursewares/showHTML',
          1: 'coursewares/showJS',
          2: 'coursewares/showVideo',
          3: 'coursewares/showZiplineOrBasejump',
          4: 'coursewares/showZiplineOrBasejump',
          5: 'coursewares/showBonfire'
        };

        saveUser(req.user)
          .subscribe(
            function() {},
            next,
            function() {
              var view = challengeView[challenge.challengeType];
              res.render(view, commonLocals);
            }
          );
      });
  }

  function completedBonfire(req, res, next) {
    debug('compltedBonfire');
    var completedWith = req.body.challengeInfo.completedWith || false;
    var challengeId = req.body.challengeInfo.challengeId;

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: Math.round(+new Date()),
      solution: req.body.challengeInfo.solution,
      challengeType: 5
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: ('' + completedWith).toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        debug('paired with ', pairedWith);
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(
        Rx.Observable.just(req.user),
        function(pairedWith, user) {
          return {
            user: user,
            pairedWith: pairedWith
          };
        }
      )
      // side effects should always be done in do's and taps
      .doOnNext(function(dats) {
        updateUserProgress(
          dats.user,
          challengeId,
          dats.pairedWith ?
            // paired programmer found and adding to data
            assign({ completedWith: dats.pairedWith.id }, challengeData) :
            // user said they paired, but pair wasn't found
            challengeData
        );
      })
      // not iterate users
      .flatMap(function(dats) {
        debug('flatmap');
        return Rx.Observable.from([dats.user, dats.pairedWith]);
      })
      // save user
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          debug('onNext');
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          debug('completed');
          return res.status(200).send(true);
        }
      );
  }

  function completedChallenge(req, res, next) {

    const completedDate = Math.round(+new Date());
    const { id, name } = req.body;
    const { challengeId, challengeName } = req.body.challengeInfo || {};

    updateUserProgress(
      req.user,
      id || challengeId,
      {
        id: id || challengeId,
        completedDate: completedDate,
        name: name || challengeName,
        solution: null,
        githubLink: null,
        verified: true
      }
    );

    saveUser(req.user)
      .subscribe(
        function(user) { debug('user save', user && user.progressTimestamps); },
        next,
        function() {
          res.sendStatus(200);
        }
      );
  }

  function completedZiplineOrBasejump(req, res, next) {

    var completedWith = req.body.challengeInfo.completedWith || '';
    var completedDate = Math.round(+new Date());
    var challengeId = req.body.challengeInfo.challengeId;
    var solutionLink = req.body.challengeInfo.publicURL;

    var githubLink = req.body.challengeInfo.challengeType === '4' ?
      req.body.challengeInfo.githubURL :
      true;

    var challengeType = req.body.challengeInfo.challengeType === '4' ?
      4 :
      3;

    if (!solutionLink || !githubLink) {
      req.flash('errors', {
        msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
        'your work.'
      });
      return res.sendStatus(403);
    }

    var challengeData = {
      id: challengeId,
      name: req.body.challengeInfo.challengeName,
      completedDate: completedDate,
      solution: solutionLink,
      githubLink: githubLink,
      challengeType: challengeType,
      verified: false
    };

    observableQueryFromModel(
        User,
        'findOne',
        { where: { username: completedWith.toLowerCase() } }
      )
      .doOnNext(function(pairedWith) {
        if (pairedWith) {
          updateUserProgress(
            pairedWith,
            challengeId,
            assign({ completedWith: req.user.id }, challengeData)
          );
        }
      })
      .withLatestFrom(Rx.Observable.just(req.user), function(pairedWith, user) {
        return {
          user: user,
          pairedWith: pairedWith
        };
      })
      .doOnNext(function({ user, pairedWith }) {
        updateUserProgress(
          user,
          challengeId,
          pairedWith ?
            assign({ completedWith: pairedWith.id }, challengeData) :
            challengeData
        );
      })
      .flatMap(function({ user, pairedWith }) {
        return Rx.Observable.from([user, pairedWith]);
      })
      // save users
      .flatMap(function(user) {
        // save user will do nothing if user is falsey
        return saveUser(user);
      })
      .subscribe(
        function(user) {
          if (user) {
            debug('user %s saved', user.username);
          }
        },
        next,
        function() {
          return res.status(200).send(true);
        }
      );
  }

  function challengeMap(req, res, next) {
    var completedList = [];

    if (req.user) {
      completedList = req.user.completedChallenges;
    }

    var noDuplicatedChallenges = R.uniq(completedList);

    var completedChallengeList = noDuplicatedChallenges
      .map(function(challenge) {
        // backwards compatibility
        return (challenge.id || challenge._id);
      });
    var challengeList = utils.
      getChallengeMapForDisplay(completedChallengeList);

    Object.keys(challengeList).forEach(function(key) {
      challengeList[key].completed = challengeList[key]
        .challenges.filter(function(elem) {
        // backwards compatibility hack
        return completedChallengeList.indexOf(elem.id || elem._id) > -1;
      });
    });

    function numberWithCommas(x) {
      return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    }

    var date1 = new Date('10/15/2014');
    var date2 = new Date();
    var timeDiff = Math.abs(date2.getTime() - date1.getTime());
    var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24));

    User.count(function(err, camperCount) {
      if (err) { return next(err); }

      res.render('challengeMap/show', {
        daysRunning: daysRunning,
        camperCount: numberWithCommas(camperCount),
        title: "A map of all Free Code Camp's Challenges",
        challengeList: challengeList,
        completedChallengeList: completedChallengeList
      });
    });
  }
};
<|endoftext|>