summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorJules Laplace <jules@okfoc.us>2014-07-02 16:22:51 -0400
committerJules Laplace <jules@okfoc.us>2014-07-02 16:22:51 -0400
commit221a14315b09946db2485036bbf4a80295dc4889 (patch)
treea0c96d11dac2aed77cc453e2fcb9841fcae4d19e /server
parentbde9f3e47d66a1c8a334763146671320ac0359f9 (diff)
password reset / username dupe stuff from PH
Diffstat (limited to 'server')
-rw-r--r--server/index.js8
-rw-r--r--server/lib/auth.js201
-rw-r--r--server/lib/auth/index.js331
-rw-r--r--server/lib/auth/mail.js67
-rw-r--r--server/lib/auth/views.js33
-rw-r--r--server/lib/middleware.js1
-rw-r--r--server/lib/schemas/User.js33
-rw-r--r--server/lib/upload.js4
-rw-r--r--server/lib/util.js6
9 files changed, 474 insertions, 210 deletions
diff --git a/server/index.js b/server/index.js
index b008131..bb242ee 100644
--- a/server/index.js
+++ b/server/index.js
@@ -61,6 +61,7 @@ site.setup = function(){
app.use(passport.initialize());
app.use(passport.session());
app.use(app.router);
+ app.enable('trust proxy')
app.get('env') === 'development' && app.use(express.errorHandler());
// Essential middleware
@@ -90,6 +91,13 @@ site.route = function () {
app.get('/auth/twitter/callback', auth.loggedIn('twitter'));
app.get('/auth/facebook', auth.login('facebook'));
app.get('/auth/facebook/callback', auth.loggedIn('facebook'));
+ app.get('/auth/usernameTaken', auth.views.usernameTaken);
+ app.post('/auth/usernameTaken', auth.usernameFixed);
+ app.get('/auth/password', auth.views.resetPassword);
+ app.post('/auth/password', auth.resetPassword);
+ app.post('/auth/passwordForgot', auth.forgotPassword);
+
+
app.get('/profile', views.profile)
app.get('/profile/edit', views.profile)
app.get('/profile/:name', views.profile)
diff --git a/server/lib/auth.js b/server/lib/auth.js
deleted file mode 100644
index 018c9ad..0000000
--- a/server/lib/auth.js
+++ /dev/null
@@ -1,201 +0,0 @@
-/* jshint node: true */
-
-var passport = require('passport'),
- FacebookStrategy = require('passport-facebook').Strategy,
- TwitterStrategy = require('passport-twitter').Strategy,
- LocalStrategy = require('passport-local').Strategy,
- passportSocketIo = require("passport.socketio"),
- cookieParser = require('express').cookieParser,
- crypto = require('crypto'),
- _ = require('lodash'),
- util = require('./util'),
- config = require('../../config.json'),
- User = require('./schemas/User');
-
-var auth = {
-
- init: function () {
- passport.serializeUser(auth.serializeUser);
- passport.deserializeUser(auth.deserializeUser);
-
- passport.use(new LocalStrategy(auth.verifyLocalUser))
-
- passport.use(new TwitterStrategy({
- consumerKey: process.env.VVALLS_TWITTER_KEY || '0L5blfBIapqhpons8bCXdIoGM',
- consumerSecret: process.env.VVALLS_TWITTER_SECRET || '5EKW7m7inoODqYSKbp7cadBKFp1FghBl4MBDoXNcUjKtodZfuP',
- callbackURL: 'http://' + config.host + '/auth/twitter/callback'
- }, auth.insertTwitterUser));
-
- passport.use(new FacebookStrategy({
- clientID: process.env.VVALLS_FACEBOOK_KEY || '719828821410310',
- clientSecret: process.env.VVALLS_FACEBOOK_SECRET || 'f9aba78e08f37f621eadb88b1409d48c',
- callbackURL: 'http://' + config.host + '/auth/facebook/callback',
- enableProof: false,
- }, auth.insertFacebookUser));
- },
-
- initSockets: function (io, express, SessionStore) {
- io.set('authorization', passportSocketIo.authorize({
- cookieParser: cookieParser,
- passport: passport,
- key: 'vvalls.sid', // the name of the cookie where express/connect stores its session_id
- secret: 'flibbertigibbet', // the session_secret to parse the cookie
- store: SessionStore, // we NEED to use a sessionstore. no memorystore please
- success: auth.socketSuccess,
- fail: auth.socketFail,
- }));
- },
-
- socketSuccess: function (data, accept) {
- // console.error('successful connection to socket.io');
- accept(null, true);
- },
-
- socketFail: function (data, message, error, accept){
- if (error) {
- throw new Error(message);
- }
- // console.log(data)
- console.error('failed connection to socket.io:', message);
-
- // We use this callback to log all of our failed connections.
- accept(null, false);
- },
-
- // technically these return the login middleware
- login: function (strategy) {
- return passport.authenticate(strategy);
- },
-
- loggedIn: function (strategy) {
- return passport.authenticate(strategy, {
- successReturnToOrRedirect: '/',
- failureRedirect: '/login'
- });
- },
-
- loggedInLocal: function (req, res, next) {
- passport.authenticate("local", function(err, user, info){
- if (err) {
- return res.json({ error: err });
- }
- if (! user) {
- return info ? res.json(info) : res.redirect("/login");
- }
- req.logIn(user, function(err) {
- if (err) { return next(err); }
- return res.json({ status: "OK" })
- });
- })(req, res, next);
- },
-
- logout: function (req, res) {
- req.logout();
- res.redirect('/');
- },
-
- serializeUser: function (user, done) {
- done(null, user._id);
- },
-
- deserializeUser: function (id, done) {
- User.findOne({ _id: id }, "_id displayName username photo isStaff", function (err, user) {
- done(err, user);
- });
- },
-
- signup: function (req, res){
- var username = util.trim(req.body.username)
- var password = req.body.password
- var email = util.trim(req.body.email)
-
- var shasum = crypto.createHash('sha1')
- shasum.update(password)
- password = shasum.digest('hex');
-
- User.findOne({ username: username }, "_id username", function (err, user) {
- if (user) {
- res.json({ error: { errors: { username: { message: "Username has been taken" } } } })
- return
- }
- var data = {
- username: username,
- displayName: username,
- password: password,
- email: email,
- created_ip: util.ip2num(req.connection.remoteAddress),
- last_ip: util.ip2num(req.connection.remoteAddress),
- created_at: new Date ()
- }
- new User(data).save(function(err, user){
- if (err || ! data) { return res.json({ error: err }) }
-
- req.login(user, function(){
- res.json({ status: "OK", payload: user })
- })
- })
- })
- },
-
- verifyLocalUser: function (username, password, done) {
- User.findOne({ username: username }, function(err, user){
- if (err) { return done(err); }
- if (!user) {
- return done(null, false, { error: { errors: { username: { message: 'Incorrect username.' } }}})
- }
- if (! user.validPassword(password)) {
- return done(null, false, { error: { errors: { password: { message: 'Incorrect password.' } }}})
- }
- return done(null, user);
- });
- },
-
- insertTwitterUser: function (accessToken, refreshToken, profile, done) {
- process.nextTick(function () {
- var userData = {
- twitter_id: profile.id,
- username: profile.username,
- displayName: profile.displayName,
- photo: profile.photos[0].value,
- twitterName: profile.username,
- };
-
- User.findOne({twitter_id: profile.id}, function(err, data){
- if (! err && data) {
- return done(err, data);
- }
- new User(userData).save(function(err, data){
- if (err) { console.error(err) }
- return done(err, data)
- })
- });
-
- });
- },
-
- insertFacebookUser: function (accessToken, refreshToken, profile, done) {
- process.nextTick(function () {
- var userData = {
- facebook_id: profile.id,
- username: profile.username || profile.displayName.toLowerCase().replace(/ /g,'-'),
- displayName: profile.displayName,
- photo: "http://graph.facebook.com/" + profile.id + "/picture?type=large",
- facebookUrl: profile.username ? "https://facebook.com/" + profile.username : ""
- };
-
- User.findOne({facebook_id: profile.id}, function(err, data){
- if (! err && data) {
- return done(err, data);
- }
- new User(userData).save(function(err, data){
- if (err) { console.error(err) }
- return done(err, data)
- })
- });
-
- });
- },
-
-}
-
-module.exports = auth
diff --git a/server/lib/auth/index.js b/server/lib/auth/index.js
new file mode 100644
index 0000000..99af9b5
--- /dev/null
+++ b/server/lib/auth/index.js
@@ -0,0 +1,331 @@
+/* jshint node: true */
+
+var passport = require('passport'),
+ FacebookStrategy = require('passport-facebook').Strategy,
+ TwitterStrategy = require('passport-twitter').Strategy,
+ LocalStrategy = require('passport-local').Strategy,
+ passportSocketIo = require("passport.socketio"),
+ cookieParser = require('express').cookieParser,
+ crypto = require('crypto'),
+ express = require('express'),
+ _ = require('lodash'),
+ mongoose = require('mongoose'),
+ util = require('../util'),
+ config = require('../../../config.json'),
+ User = require('../schemas/User');
+
+var auth = {
+ views: require('./views'),
+ mail: require('./mail'),
+
+ init: function () {
+ passport.serializeUser(auth.serializeUser);
+ passport.deserializeUser(auth.deserializeUser);
+
+ passport.use(new LocalStrategy(auth.verifyLocalUser))
+
+ passport.use(new TwitterStrategy({
+ consumerKey: process.env.VVALLS_TWITTER_KEY || 'brI5VqBak5yrhCcxU56lj5L3v',
+ consumerSecret: process.env.VVALLS_TWITTER_SECRET || 'ThzaEVWUgkmfzqOs3qcrdonGzgDBjDHTVzPkfY0wFJxjUH6JWZ',
+ callbackURL: 'http://' + config.host + '/auth/twitter/callback'
+ }, auth.insertTwitterUser));
+
+ /*
+ passport.use(new FacebookStrategy({
+ clientID: process.env.VVALLS_FACEBOOK_KEY || '719828821410310',
+ clientSecret: process.env.VVALLS_FACEBOOK_SECRET || 'f9aba78e08f37f621eadb88b1409d48c',
+ callbackURL: 'http://' + config.host + '/auth/facebook/callback',
+ enableProof: false,
+ }, auth.insertFacebookUser));
+ */
+ auth.mail.init()
+ },
+
+ initSockets: function (io, SessionStore) {
+ io.set('authorization', passportSocketIo.authorize({
+ cookieParser: express.cookieParser,
+ passport: passport,
+ key: 'vvalls.sid', // the name of the cookie where express/connect stores its session_id
+ secret: 'flibbertigibbet', // the session_secret to parse the cookie
+ store: SessionStore, // we NEED to use a sessionstore. no memorystore please
+ success: auth.socketSuccess,
+ fail: auth.socketFail,
+ }));
+ },
+
+ socketSuccess: function (data, accept) {
+ // console.error('successful connection to socket.io');
+ accept(null, true);
+ },
+
+ socketFail: function (data, message, error, accept){
+ if (error) {
+ throw new Error(message);
+ }
+ // console.log(data)
+ console.error('failed connection to socket.io:', message);
+
+ // We use this callback to log all of our failed connections.
+ accept(null, false);
+ },
+
+ // technically these return the login middleware
+ login: function (strategy) {
+ return passport.authenticate(strategy);
+ },
+
+ loggedIn: function (strategy) {
+ return function (req, res, next) {
+ console.log("attempting to use", strategy)
+
+ passport.authenticate(strategy, function(err, user, info){
+ if (err) {
+ return next(err);
+ }
+ if (! user) {
+ req.session.userData = info
+ return res.redirect('/auth/usernameTaken');
+ }
+ if (! user.created_ip) {
+ user.created_ip = util.ip2num( req.ip )
+ }
+ user.last_ip = util.ip2num( req.ip )
+ user.save(function(err, data){ if (err) console.err('error setting ip for user') })
+
+ req.logIn(user, function(err) {
+ if (err) { return next(err); }
+ var returnTo = req.session.returnTo
+ delete req.session.returnTo
+ return res.redirect( returnTo || "/profile" );
+ });
+ })(req, res, next);
+ }
+ },
+
+ loggedInLocal: function (req, res, next) {
+ passport.authenticate("local", function(err, user, info){
+ if (err) {
+ return res.json({ error: err });
+ }
+ if (! user) {
+ return info ? res.json(info) : res.redirect("/login");
+ }
+
+ user.last_ip = util.ip2num( req.ip )
+ user.save(function(err, data){ if (err) console.err('error setting ip for user') })
+
+ req.logIn(user, function(err) {
+ if (err) { return next(err); }
+ var returnTo = req.session.returnTo
+ delete req.session.returnTo
+ return res.json({ status: "OK", returnTo: returnTo || "/profile" })
+ });
+ })(req, res, next);
+ },
+
+ logout: function (req, res) {
+ req.logout();
+ res.redirect('/');
+ },
+
+ serializeUser: function (user, done) {
+ done(null, user._id);
+ },
+
+ deserializeUser: function (id, done) {
+ try {
+ var _id = mongoose.Types.ObjectId(id)
+ User.findOne({ _id: _id }, "_id displayName username photo isStaff", function (err, user) {
+ done(err, user);
+ });
+ }
+ catch (e) {
+ User.findOne({ twitter_id: id }, "_id displayName username photo isStaff", function (err, user) {
+ done(err, user);
+ });
+ }
+ },
+
+ signup: function (req, res){
+ var username = util.trim(req.body.username)
+ var password = req.body.password
+ var email = util.trim(req.body.email)
+
+ var shasum = crypto.createHash('sha1')
+ shasum.update(password)
+ password = shasum.digest('hex');
+
+ User.findByUsername(username, function (err, user) {
+ if (user) {
+ res.json({ error: { errors: { username: { message: "Username has been taken" } } } })
+ return
+ }
+
+ User.findByEmail(email, function (err, user) {
+ if (user) {
+ res.json({ error: { errors: { username: { message: "Email has already been used" } } } })
+ return
+ }
+ var data = {
+ username: username,
+ displayName: username,
+ password: password,
+ email: email,
+ created_ip: util.ip2num( req.ip ),
+ last_ip: util.ip2num( req.ip ),
+ created_at: new Date ()
+ }
+ new User(data).save(function(err, user){
+ if (err || ! data) { return res.json({ error: err }) }
+ req.login(user, function(){
+ auth.mail.welcome(user, function(){
+ res.json({ status: "OK", payload: user })
+ })
+ })
+ })
+ })
+ })
+ },
+
+ verifyLocalUser: function (username, password, done) {
+ User.findByUsername(username, function(err, user){
+ if (err) { return done(err); }
+ if (! user) {
+ return done(null, false, { error: { errors: { username: { message: 'No such username.' } }}})
+ }
+ if (! user.validPassword(password)) {
+ return done(null, false, { error: { errors: { password: { message: 'Incorrect password.' } }}})
+ }
+ return done(null, user);
+ });
+ },
+
+ insertTwitterUser: function (accessToken, refreshToken, profile, done) {
+ process.nextTick(function () {
+ var userData = {
+ twitter_id: profile.id,
+ username: profile.username,
+ displayName: profile.displayName,
+ photo: profile.photos[0].value,
+ twitterName: profile.username,
+ };
+ User.findOne({twitter_id: profile.id}, function(err, data){
+ if (! err && data) {
+ return done(err, data);
+ }
+ User.findByUsername(profile.username, function(err, data){
+ if (data) {
+ return done(null, false, userData)
+ }
+ new User(userData).save(function(err, data){
+ if (err) {
+ console.error(err)
+ }
+ return done(err, data)
+ })
+ })
+ });
+
+ });
+ },
+
+ insertFacebookUser: function (accessToken, refreshToken, profile, done) {
+ process.nextTick(function () {
+ var userData = {
+ facebook_id: profile.id,
+ username: profile.username || profile.displayName.toLowerCase().replace(/ /g,'-'),
+ displayName: profile.displayName,
+ photo: "http://graph.facebook.com/" + profile.id + "/picture?type=large",
+ facebookUrl: profile.username ? "https://facebook.com/" + profile.username : ""
+ };
+
+ User.findOne({twitter_id: profile.id}, function(err, data){
+ if (! err && data) {
+ return done(err, data);
+ }
+ User.findByUsername(profile.username, function(err, data){
+ if (data) {
+ return done(null, false, userData)
+ }
+ new User(userData).save(function(err, data){
+ if (err) {
+ console.error(err)
+ }
+ return done(err, data)
+ })
+ })
+ });
+
+ });
+ },
+
+ usernameFixed: function (req, res) {
+
+ var userData = req.session.userData
+ if (! userData) {
+ return res.redirect("/")
+ }
+ if (req.isAuthenticated()) {
+ delete req.session.userData
+ return res.redirect("/")
+ }
+
+ var username = util.sanitize(req.body.username)
+
+ User.findByUsername(username, function(err, doc){
+ if (err || doc) {
+ res.json({ error: { errors: { username: { message: "Username has been taken" } } } })
+ return
+ }
+ userData.username = username
+
+ new User(userData).save(function(err, user){
+ req.logIn(user, function(err) {
+ if (err) { return res.json(err); }
+ var returnTo = req.session.returnTo
+ delete req.session.returnTo
+ return res.json({ status: "OK", returnTo: returnTo || "/profile" })
+ });
+ })
+ })
+ },
+
+ forgotPassword: function (req, res) {
+ User.findByEmail(req.body.email, function(err, user){
+ if (err || ! user) {
+ res.json({ error: { errors: { email: { message: "That email address was not found." } } } })
+ return
+ }
+ User.resetPasswordNonce(user, function(err){
+ auth.mail.forgotPassword(user, function(){
+ res.json({ success: 'OK' })
+ })
+ })
+ })
+ },
+
+ resetPassword: function (req, res) {
+ var password = req.body.password
+
+ var shasum = crypto.createHash('sha1')
+ shasum.update(password)
+ password = shasum.digest('hex');
+
+ User.findOne({ passwordNonce: req.body.nonce }, function (err, user){
+ if (err || ! user) {
+ res.json({ error: { errors: { email: { message: "That reset token has already been used." } } } })
+ return
+ }
+ user.password = password
+ user.passwordNonce = ""
+ user.save(function(){
+ req.login(user, function(){
+ res.json({ status: "OK", payload: user })
+ })
+ })
+ })
+ },
+
+}
+
+module.exports = auth
diff --git a/server/lib/auth/mail.js b/server/lib/auth/mail.js
new file mode 100644
index 0000000..a4abccd
--- /dev/null
+++ b/server/lib/auth/mail.js
@@ -0,0 +1,67 @@
+
+var email = require("emailjs"),
+ ejs = require("ejs"),
+ fs = require("fs"),
+ util = require("../util");
+
+var mail = {
+
+ from: 'Vvalls <info@vvalls.com>',
+ templates: {},
+
+ init: function(){
+ var names = ["welcome","password"].forEach(function(name){
+ mail.templates[name] = {};
+ var types = ["text","html"].forEach(function(type){
+ fs.readFile("views/mail/" + name + "." + type + ".ejs", function(err, data){
+ mail.templates[name][type] = ejs.compile(data.toString())
+ })
+ })
+ })
+ },
+
+ connect: function(){
+ var server = email.server.connect({
+ user: process.env.OKFOCUS_EMAIL_USERNAME,
+ password: process.env.OKFOCUS_EMAIL_PASSWORD,
+ host: "smtp.sendgrid.net",
+ ssl: true
+ })
+ return server
+ },
+
+ send: function(msg, cb){
+ var server = mail.connect()
+ server.send(msg, cb)
+ },
+
+ welcome: function(user, cb){
+ var message = {
+ text: mail.templates.welcome.text(user),
+ from: mail.from,
+ to: user.email,
+ subject: "Welcome to Vvalls",
+ attachment: [
+ { data: mail.templates.welcome.html(user), alternative: true },
+ ]
+ };
+ mail.send(message, cb)
+ console.log("sent welcome email to", user.email)
+ },
+
+ forgotPassword: function(user, cb){
+ var message = {
+ text: mail.templates.password.text(user),
+ from: mail.from,
+ to: user.email,
+ subject: "Recover your password",
+ attachment: [
+ { data: mail.templates.password.html(user), alternative: true },
+ ]
+ }
+ mail.send(message, cb)
+ console.log("sent password email to", user.email)
+ },
+}
+
+module.exports = mail
diff --git a/server/lib/auth/views.js b/server/lib/auth/views.js
new file mode 100644
index 0000000..591b06a
--- /dev/null
+++ b/server/lib/auth/views.js
@@ -0,0 +1,33 @@
+
+var util = require('../util'),
+ User = require("../schemas/User");
+
+
+var views = {}
+
+views.usernameTaken = function (req, res) {
+ var userData = req.session.userData
+ if (! userData) {
+ return res.redirect("/")
+ }
+ if (req.isAuthenticated()) {
+ delete req.session.userData
+ return res.redirect("/")
+ }
+ res.render("modal", { opt: { username: util.sanitize(userData.username) } })
+}
+
+views.resetPassword = function (req, res) {
+ var nonce = util.sanitize(req.query.nonce)
+ if (! nonce.length) {
+ return res.redirect("/")
+ }
+ User.findOne({ passwordNonce: nonce }, function (err, user){
+ if (err || ! user) {
+ return res.redirect("/")
+ }
+ res.render("reset-password", { username: user.username, nonce: user.passwordNonce })
+ })
+}
+
+module.exports = views
diff --git a/server/lib/middleware.js b/server/lib/middleware.js
index 0bc3f7a..aec54ad 100644
--- a/server/lib/middleware.js
+++ b/server/lib/middleware.js
@@ -39,6 +39,7 @@ var middleware = {
res.locals.user = req.user || {}
res.locals.config = config
res.locals.profile = null
+ res.locals.opt = {}
next()
},
diff --git a/server/lib/schemas/User.js b/server/lib/schemas/User.js
index 77e2e02..b64f8fc 100644
--- a/server/lib/schemas/User.js
+++ b/server/lib/schemas/User.js
@@ -41,10 +41,8 @@ var UserSchema = new mongoose.Schema({
}, "{PATH} is not an acceptable name"]
},
email: { type: String, default: "" },
- emailVerified: {
- type: Boolean,
- default: false,
- },
+ emailVerified: { type: Boolean, default: false, },
+ emailOptout: { type: Boolean, default: false, },
password: {
type: String,
validate: [function (val) {
@@ -52,6 +50,11 @@ var UserSchema = new mongoose.Schema({
return true
}, "{PATH} is not an acceptable password"]
},
+ passwordNonce: {
+ type: String,
+ default: "",
+ },
+
location: { type: String, default: "" },
photo: { type: String, default: "" },
bio: { type: String, default: "" },
@@ -72,6 +75,26 @@ UserSchema.methods.validPassword = function (pw) {
shasum.update(pw)
return this.password === shasum.digest('hex');
}
-
+UserSchema.statics.findByUsername = function (username, cb) {
+ this.findOne({ username: new RegExp("^" + username + "$", "i") }, cb)
+}
+UserSchema.statics.findByEmail = function (email, cb) {
+ email = util.escapeRegExp(email)
+ this.findOne({ email: new RegExp("^" + email + "$", "i") }, cb)
+}
+UserSchema.statics.findByIP = function (ip, cb) {
+ ip = util.ip2num(ip)
+ this.findOne({ $or: [{ created_ip: ip }, { last_ip: ip }] }, cb)
+}
+UserSchema.statics.resetPasswordNonce = function(user, cb){
+ crypto.pseudoRandomBytes(256, function (err, buf){
+ var shasum = crypto.createHash('sha1')
+ shasum.update(buf)
+ user.passwordNonce = shasum.digest('hex')
+ user.save(function(err, doc){
+ cb()
+ })
+ })
+}
module.exports = exports = mongoose.model('user', UserSchema);
exports.schema = UserSchema;
diff --git a/server/lib/upload.js b/server/lib/upload.js
index 0f6c624..e206f7c 100644
--- a/server/lib/upload.js
+++ b/server/lib/upload.js
@@ -60,7 +60,9 @@ module.exports.put = function (key, file, opt) {
}, function(err, s3res) {
if (err || s3res.statusCode !== 200) {
console.error(err);
- s3res.resume()
+ if (s3res && s3res.resume) {
+ s3res.resume()
+ }
return;
}
diff --git a/server/lib/util.js b/server/lib/util.js
index aaa8274..6604abe 100644
--- a/server/lib/util.js
+++ b/server/lib/util.js
@@ -16,18 +16,18 @@ util.trim = function (s){ return (s || "").replace(whitespaceHead,"").replace(wh
util.slugify = function (s){
return (s || "").toLowerCase().replace(whitespace,"-").replace(nonAlphanumerics, '-').replace(consecutiveDashes,"-")
}
-
util.sanitize = function (s){
return (s || "").replace(entities, "")
}
-
util.capitalize = function (s) {
return (s || "").split(" ").map(util.capitalizeWord).join(" ");
}
-
util.capitalizeWord = function (s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
+util.escapeRegExp: function (s) {
+ return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
+}
util.cleanQuery = function (query) {