diff --git a/opt/comments-server-side/.gitignore b/opt/comments-server-side/.gitignore deleted file mode 100644 index 5cfbe7d1f..000000000 --- a/opt/comments-server-side/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -config.js -node_modules diff --git a/opt/comments-server-side/ForumUser.js b/opt/comments-server-side/ForumUser.js deleted file mode 100644 index 561c7a29e..000000000 --- a/opt/comments-server-side/ForumUser.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Authentication with a vBulletin user database - */ - -var crypto = require('crypto'), - _ = require('underscore'); - -var ForumUser = exports.ForumUser = function(client) { - this.client = client; -}; - -ForumUser.prototype = { - - login: function(username, password, callback) { - var sql = "SELECT userid, usergroupid, membergroupids, email, username, password, salt FROM user WHERE username = ?", - self = this; - - this.client.query(sql, [username], function(err, results, fields) { - if (err) { - callback(err); - return; - } - - if (results.length == 0) { - callback("No such user"); - return; - } - - var user = results[0]; - - if (!self.checkPassword(password, user.salt, user.password)) { - callback("Invalid password"); - return; - } - - user.moderator = self.isModerator(user); - - callback(null, user); - }); - }, - - clientUser: function(user) { - return { - emailHash: crypto.createHash('md5').update(user.email).digest("hex"), - userName: user.username, - userId: user.userid, - mod: user.moderator - }; - }, - - checkPassword: function(password, salt, saltedPassword) { - password = crypto.createHash('md5').update(password).digest("hex") + salt; - password = crypto.createHash('md5').update(password).digest("hex"); - - return password == saltedPassword; - }, - - isModerator: function(user) { - var COMMUNITY_SUPPORT_TEAM = 2; - var DEV_TEAM = 19; - - if (typeof user.membergroupids === "string") { - var ids = _.map(user.membergroupids.split(','), parseInt); - } - else { - var ids = []; - } - - return _.include(ids, COMMUNITY_SUPPORT_TEAM) || _.include(ids, DEV_TEAM); - } -}; diff --git a/opt/comments-server-side/app.js b/opt/comments-server-side/app.js deleted file mode 100644 index e798982b4..000000000 --- a/opt/comments-server-side/app.js +++ /dev/null @@ -1,379 +0,0 @@ - -/** - * JSDuck authentication / commenting server side element. Requires Node.js + MongoDB. - * - * Authentication assumes a vBulletin forum database, but could easily be adapted (see ForumUser.js) - * - * Expects a config file, config.js, that looks like this: - * - * exports.db = { - * user: 'forumUsername', - * password: 'forumPassword', - * host: 'forumHost', - * dbName: 'forumDb' - * }; - * - * exports.sessionSecret = 'random string for session cookie encryption'; - * - * exports.mongoDb = 'mongodb://mongoHost:port/comments'; - * - */ - -var config = require('./config'); -require('./database'); - -var mysql = require('mysql'), - client = mysql.createClient({ - host: config.db.host, - user: config.db.user, - password: config.db.password, - database: config.db.dbName - }), - express = require('express'), - MongoStore = require('connect-mongo'), - _ = require('underscore'), - ForumUser = require('./ForumUser').ForumUser, - forumUser = new ForumUser(client), - util = require('./util'), - crypto = require('crypto'), - mongoose = require('mongoose'); - -var app = express(); - -app.configure(function() { - - // Headers for Cross Origin Resource Sharing (CORS) - app.use(function (req, res, next) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'); - next(); - }); - - app.use(express.cookieParser(config.sessionSecret)); - - // Hack to set session cookie if session ID is set as a URL param. - // This is because not all browsers support sending cookies via CORS - app.use(function(req, res, next) { - if (req.query.sid && req.query.sid != 'null') { - var sid = req.query.sid.replace(/ /g, '+'); - req.sessionID = sid; - req.signedCookies = req.signedCookies || {}; - req.signedCookies['sencha_docs'] = sid; - } - next(); - }); - - // Use MongoDB for session storage - app.use(express.session({ - secret: config.sessionSecret, - key: 'sencha_docs', - store: new MongoStore({ - url: exports.mongoDb + "/sessions" - }) - })); - - app.use(function(req, res, next) { - // IE doesn't get content-type, so default to form encoded. - if (!req.headers['content-type']) { - req.headers['content-type'] = 'application/x-www-form-urlencoded'; - } - next(); - }); - - app.use(express.bodyParser()); - app.use(express.methodOverride()); - - app.enable('jsonp callback'); -}); - -app.configure('development', function(){ - app.use(express.logger('dev')); - app.use(express.errorHandler()); -}); - -/** - * Authentication - */ - -app.get('/auth/session', function(req, res) { - var result = req.session && req.session.user && forumUser.clientUser(req.session.user); - res.json(result || false); -}); - -app.post('/auth/login', function(req, res){ - - forumUser.login(req.body.username, req.body.password, function(err, result) { - - if (err) { - res.json({ success: false, reason: err }); - return; - } - - req.session = req.session || {}; - req.session.user = result; - - var response = _.extend(forumUser.clientUser(result), { - sessionID: req.sessionID, - success: true - }); - - res.json(response); - }); -}); - -// Remove session -app.post('/auth/logout', function(req, res){ - req.session.user = null; - res.json({ success: true }); -}); - -/** - * Handles comment unsubscription requests. - */ -app.get('/auth/unsubscribe/:subscriptionId', function(req, res) { - - Subscription.findOne({ _id: req.params.subscriptionId }, function(err, subscription) { - if (err) throw(err); - - if (subscription) { - if (req.query.all == 'true') { - Subscription.remove({ userId: subscription.userId }, function(err) { - res.send("You have been unsubscribed from all threads."); - }); - } else { - Subscription.remove({ _id: req.params.subscriptionId }, function(err) { - res.send("You have been unsubscribed from that thread."); - }); - } - } else { - res.send("You are already unsubscribed."); - } - }); -}); - - -/** - * Commenting - */ - -/** - * Returns a list of comments for a particular target (eg class, guide, video) - */ -app.get('/auth/:sdk/:version/comments', util.getCommentReads, function(req, res) { - - if (!req.query.startkey) { - res.json({error: 'Invalid request'}); - return; - } - - Comment.find({ - target: JSON.parse(req.query.startkey), - deleted: { '$ne': true }, - sdk: req.params.sdk, - version: req.params.version - }).sort('createdAt', 1).run(function(err, comments){ - res.json(util.scoreComments(comments, req)); - }); -}); - -/** - * Returns n most recent comments. - * Takes two parameters: offset and limit. - * - * The last comment object returned will contain `total_rows`, - * `offset` and `limit` fields. I'd say it's a hack, but at least - * it works for now. - */ -app.get('/auth/:sdk/:version/comments_recent', util.getCommentReads, function(req, res) { - var offset = parseInt(req.query.offset, 10) || 0; - var limit = parseInt(req.query.limit, 10) || 100; - var filter = { - deleted: { '$ne': true }, - sdk: req.params.sdk, - version: req.params.version - }; - - if (req.query.hideRead && req.commentMeta.reads.length > 0) { - filter._id = { $nin: req.commentMeta.reads }; - } - - if (req.query.hideCurrentUser) { - filter.userId = { $ne: req.session.user.userid }; - } - - Comment.find(filter).sort("createdAt", -1).run(function(err, comments) { - var total_rows = comments.length; - - comments = util.scoreComments(comments, req); - - // For now this is the simplest solution to enable sorting by score. - // A bit inefficient to select all records, but it's only for - // moderators, so it shouldn't pose much of a performance problem. - if (req.query.sortByScore) { - util.sortByField(comments, "score", "DESC"); - } - comments = comments.slice(offset, offset+limit); - - // store total count to last comment - var last = comments[comments.length-1]; - if (last) { - last.total_rows = total_rows; - last.offset = offset; - last.limit = limit; - } - res.json(comments); - }); -}); - -/** - * Returns number of comments for each class/member, - * and a list of classes/members into which the user has subscribed. - */ -app.get('/auth/:sdk/:version/comments_meta', util.getCommentCounts, util.getCommentSubscriptions, function(req, res) { - res.send({ comments: req.commentCounts, subscriptions: req.commentSubscriptions || [] }); -}); - -/** - * Returns an individual comment (used when editing a comment) - */ -app.get('/auth/:sdk/:version/comments/:commentId', util.findComment, function(req, res) { - res.json({ success: true, content: req.comment.content }); -}); - -/** - * Creates a new comment - */ -app.post('/auth/:sdk/:version/comments', util.requireLoggedInUser, function(req, res) { - - var target = JSON.parse(req.body.target); - - if (target.length === 2) { - target.push(''); - } - - var comment = new Comment({ - author: req.session.user.username, - userId: req.session.user.userid, - content: req.body.comment, - rating: Number(req.body.rating), - contentHtml: util.markdown(req.body.comment), - downVotes: [], - upVotes: [], - createdAt: new Date, - target: target, - emailHash: crypto.createHash('md5').update(req.session.user.email).digest("hex"), - sdk: req.params.sdk, - version: req.params.version, - moderator: req.session.user.moderator, - title: req.body.title, - url: req.body.url - }); - - comment.saveNew(req.session.user, function(err) { - res.json({ success: true, id: comment._id }); - - util.sendEmailUpdates(comment); - }); - -}); - -/** - * Updates an existing comment (for voting or updating contents) - */ -app.post('/auth/:sdk/:version/comments/:commentId', util.requireLoggedInUser, util.findComment, function(req, res) { - - var voteDirection, - comment = req.comment; - - if (req.body.vote) { - util.vote(req, res, comment); - } else { - util.requireOwner(req, res, function() { - comment.content = req.body.content; - comment.contentHtml = util.markdown(req.body.content); - - util.logUpdate(comment, req.session.user.username); - - comment.save(function(err) { - res.json({ success: true, content: comment.contentHtml }); - }); - }); - } -}); - -/** - * Deletes a comment - */ -app.post('/auth/:sdk/:version/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) { - req.comment.deleted = true; - util.logUpdate(req.comment, req.session.user.username, "delete"); - req.comment.save(function(err) { - res.send({ success: true }); - }); -}); - -/** - * Restores deleted comment - */ -app.post('/auth/:sdk/:version/comments/:commentId/undo_delete', util.requireLoggedInUser, util.findComment, util.requireOwner, util.getCommentReads, function(req, res) { - req.comment.deleted = false; - util.logUpdate(req.comment, req.session.user.username, "undo_delete"); - req.comment.save(function(err) { - res.send({ success: true, comment: util.scoreComments([req.comment], req)[0] }); - }); -}); - -/** - * Marks a comment 'read' - */ -app.post('/auth/:sdk/:version/comments/:commentId/read', util.requireLoggedInUser, util.findCommentMeta, function(req, res) { - req.commentMeta.metaType = 'read'; - req.commentMeta.save(function(err) { - res.send({ success: true }); - }); -}); - -/** - * Get email subscriptions - */ -app.get('/auth/:sdk/:version/subscriptions', util.getCommentSubscriptions, function(req, res) { - res.json({ subscriptions: req.commentMeta.subscriptions }); -}); - -/** - * Subscibe / unsubscribe to a comment thread - */ -app.post('/auth/:sdk/:version/subscribe', util.requireLoggedInUser, function(req, res) { - - var subscriptionBody = { - sdk: req.params.sdk, - version: req.params.version, - target: JSON.parse(req.body.target), - userId: req.session.user.userid - }; - - Subscription.findOne(subscriptionBody, function(err, subscription) { - - if (subscription && req.body.subscribed == 'false') { - - subscription.remove(function(err, ok) { - res.send({ success: true }); - }); - - } else if (!subscription && req.body.subscribed == 'true') { - - subscription = new Subscription(subscriptionBody); - subscription.email = req.session.user.email; - - subscription.save(function(err) { - res.send({ success: true }); - }); - } - }); -}); - -var port = 3000; -app.listen(port); -console.log("Server started at port "+port+"..."); - diff --git a/opt/comments-server-side/database.js b/opt/comments-server-side/database.js deleted file mode 100644 index d4297a02e..000000000 --- a/opt/comments-server-side/database.js +++ /dev/null @@ -1,74 +0,0 @@ - -/** - * Defines comment schema and connects to database - */ - -var mongoose = require('mongoose'), - util = require('./util'), - config = require('./config'); - -CommentSchema = new mongoose.Schema({ - sdk: String, - version: String, - - author: String, - userId: Number, - content: String, - contentHtml: String, - createdAt: Date, - downVotes: Array, - emailHash: String, - rating: Number, - target: Array, - upVotes: Array, - deleted: Boolean, - updates: Array, - mod: Boolean, - title: String, - url: String -}); - -// Helper method for adding new comments. -// When moderator posts comment, mark it automatically as read. -CommentSchema.methods.saveNew = function(user, next) { - var comment = this; - if (user.moderator) { - comment.save(function(err) { - var meta = new Meta({ - userId: user.userid, - commentId: comment._id, - metaType: 'read' - }); - meta.save(next); - }); - } - else { - comment.save(next); - } -}; - -Comment = mongoose.model('Comment', CommentSchema); - -Subscription = mongoose.model('Subscription', new mongoose.Schema({ - sdk: String, - version: String, - - createdAt: Date, - userId: Number, - email: String, - target: Array -})); - -Meta = mongoose.model('Meta', new mongoose.Schema({ - sdk: String, - version: String, - - createdAt: Date, - userId: Number, - commentId: String, - metaType: String -})); - -mongoose.connect(config.mongoDb, function(err, ok) { - console.log("Connected to DB") -}); diff --git a/opt/comments-server-side/package.json b/opt/comments-server-side/package.json deleted file mode 100644 index 9a3e46f84..000000000 --- a/opt/comments-server-side/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "jsduck_comments", - "version": "0.5.0", - "description": "Commenting backend for JSDuck Documentation", - "author": "Nick Poudlen ", - "dependencies": { - "express": "git://github.com/visionmedia/express.git", - "express-namespace": "", - "connect": "", - "connect-mongo": "0.1.7", - "marked": "", - "mongoose": "", - "mysql": "", - "sanitizer": "", - "step": "", - "underscore": "", - "nodemailer": "" - } -} diff --git a/opt/comments-server-side/util.js b/opt/comments-server-side/util.js deleted file mode 100644 index 8db8f6c10..000000000 --- a/opt/comments-server-side/util.js +++ /dev/null @@ -1,439 +0,0 @@ - -var marked = require('marked'), - _ = require('underscore'), - sanitizer = require('sanitizer'), - nodemailer = require("nodemailer"), - mongoose = require('mongoose'); - -/** - * Converts Markdown-formatted comment text into HTML. - * - * @param {String} content Markdown-formatted text - * @return {String} HTML - */ -exports.markdown = function(content) { - var markdowned; - try { - markdowned = marked(content); - } catch(e) { - markdowned = content; - } - - // Strip dangerous markup, but allow links to all URL-s - var sanitized_output = sanitizer.sanitize(markdowned, function(str) { - return str; - }); - - // IE does not support ' - return sanitized_output.replace(/'/g, '''); -}; - -/** - * Calculates up/down scores for each comment. - * - * Marks if the current user has already voted on the comment. - * Ensures createdAt timestamp is a string. - * - * @param {Object[]} comments - * @param {Object} req Containing username data - * @return {Object[]} - */ -exports.scoreComments = function(comments, req) { - return _.map(comments, function(comment) { - comment = _.extend(comment._doc, { - score: comment.upVotes.length - comment.downVotes.length, - createdAt: String(comment.createdAt) - }); - - if (req.commentMeta.reads.length > 0) { - comment.read = _.include(req.commentMeta.reads, ""+comment._id); - } - - if (req.session.user) { - comment.upVote = _.contains(comment.upVotes, req.session.user.username); - comment.downVote = _.contains(comment.downVotes, req.session.user.username); - } - - return comment; - }); -}; - -/** - * Sorts array of objects by the value of given field. - * - * @param {Array} arr - * @param {String} field - * @param {String} [direction="ASC"] either "ASC" or "DESC". - */ -exports.sortByField = function(arr, field, direction) { - if (direction === "DESC") { - var more = -1; - var less = 1; - } - else { - var more = 1; - var less = -1; - } - - arr.sort(function(aObj, bObj) { - var a = aObj[field]; - var b = bObj[field]; - return a > b ? more : a < b ? less : 0; - }); -}; - -/** - * Performs voting on comment. - * - * @param {Object} req The request object. - * @param {Object} res The response object where voting result is written. - * @param {Comment} comment The comment to vote on. - */ -exports.vote = function(req, res, comment) { - var voteDirection; - var username = req.session.user.username; - - if (username == comment.author) { - - // Ignore votes from the author - res.json({success: false, reason: 'You cannot vote on your own content'}); - return; - - } else if (req.body.vote == 'up' && !_.include(comment.upVotes, username)) { - - var voted = _.include(comment.downVotes, username); - - comment.downVotes = _.reject(comment.downVotes, function(v) { - return v == username; - }); - - if (!voted) { - voteDirection = 'up'; - comment.upVotes.push(username); - } - } else if (req.body.vote == 'down' && !_.include(comment.downVotes, username)) { - - var voted = _.include(comment.upVotes, username); - - comment.upVotes = _.reject(comment.upVotes, function(v) { - return v == username; - }); - - if (!voted) { - voteDirection = 'down'; - comment.downVotes.push(username); - } - } - - comment.save(function(err) { - res.json({ - success: true, - direction: voteDirection, - total: (comment.upVotes.length - comment.downVotes.length) - }); - }); -}; - -/** - * Appends update record to comment updates log. - * - * @param {Object} comment The comment we're updating - * @param {String} author Author of the update - * @param {String} [action] The user action we're recording. - * Leaving this empty, means normal update. Other currently used - * actions are "delete" and "undo_delete". - */ -exports.logUpdate = function(comment, author, action) { - var up = { - updatedAt: new Date(), - author: author - }; - if (action) { - up.action = action; - } - comment.updates = comment.updates || []; - comment.updates.push(up); -}; - -/** - * Ensures that user is logged in. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.requireLoggedInUser = function(req, res, next) { - if (!req.session || !req.session.user) { - res.json({success: false, reason: 'Forbidden'}, 403); - } else { - next(); - } -}; - -/** - * Looks up comment by ID. - * - * Stores it into `req.comment`. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.findComment = function(req, res, next) { - if (req.params.commentId) { - Comment.findById(req.params.commentId, function(err, comment) { - req.comment = comment; - next(); - }); - } else { - res.json({success: false, reason: 'No such comment'}); - } -}; - -/** - * Looks up comment meta by comment ID. - * - * Stores it into `req.commentMeta`. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.findCommentMeta = function(req, res, next) { - if (req.params.commentId) { - - var userCommentMeta = { - userId: req.session.user.userid, - commentId: req.params.commentId - }; - - Meta.findOne(userCommentMeta, function(err, commentMeta) { - req.commentMeta = commentMeta || new Meta(userCommentMeta); - next(); - }); - } else { - res.json({success: false, reason: 'No such comment'}); - } -}; - -/** - * True if the user is author of the comment - */ -function isAuthor(user, comment) { - return user.username === comment.author; -} -exports.isAuthor = isAuthor; - -/** - * Ensures that user is allowed to modify/delete the comment, - * that is, he is the owner of the comment or a moderator. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.requireOwner = function(req, res, next) { - if (req.session.user.moderator || isAuthor(req.session.user, req.comment)) { - next(); - } - else { - res.json({ success: false, reason: 'Forbidden' }, 403); - } -}; - -/** - * Sends e-mail updates when comment is posted to a thread that has - * subscribers. - * - * @param {Comment} comment - */ -exports.sendEmailUpdates = function(comment) { - var mailTransport = nodemailer.createTransport("SMTP",{ - host: 'localhost', - port: 25 - }); - - var sendSubscriptionEmail = function(emails) { - var email = emails.shift(); - - if (email) { - nodemailer.sendMail(email, function(err){ - if (err){ - console.log(err); - } else{ - console.log("Sent email to " + email.to); - sendSubscriptionEmail(emails); - } - }); - } else { - console.log("Finished sending emails"); - mailTransport.close(); - } - }; - - var subscriptionBody = { - sdk: comment.sdk, - version: comment.version, - target: comment.target - }; - - var emails = []; - - Subscription.find(subscriptionBody, function(err, subscriptions) { - _.each(subscriptions, function(subscription) { - var mailOptions = { - transport: mailTransport, - from: "Sencha Documentation ", - to: subscription.email, - subject: "Comment on '" + comment.title + "'", - text: [ - "A comment by " + comment.author + " on '" + comment.title + "' was posted on the Sencha Documentation:\n", - comment.content + "\n", - "--", - "Original thread: " + comment.url, - "Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id, - "Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true' - ].join("\n") - }; - - if (Number(comment.userId) != Number(subscription.userId)) { - emails.push(mailOptions); - } - }); - - if (emails.length) { - sendSubscriptionEmail(emails); - } else { - console.log("No emails to send"); - } - }); -}; - -/** - * Retrieves comment counts for each target. - * - * Stores into `req.commentCounts` field an array like this: - * - * [ - * {"_id": "class__Ext__", "value": 3}, - * {"_id": "class__Ext__method-define", "value": 1}, - * {"_id": "class__Ext.Panel__cfg-title", "value": 8} - * ] - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.getCommentCounts = function(req, res, next) { - // Map each comment into: ("type__Class__member", 1) - var map = function() { - if (this.target) { - emit(this.target.slice(0,3).join('__'), 1); - } else { - return; - } - }; - - // Sum comment counts for each target - var reduce = function(key, values) { - var total = 0; - - for (var i = 0; i < values.length; i++) { - total += values[i]; - } - - return total; - }; - - mongoose.connection.db.executeDbCommand({ - mapreduce: 'comments', - map: map.toString(), - reduce: reduce.toString(), - out: 'commentCounts', - query: { - deleted: { '$ne': true }, - sdk: req.params.sdk, - version: req.params.version - } - }, function(err, dbres) { - mongoose.connection.db.collection('commentCounts', function(err, collection) { - collection.find({}).toArray(function(err, comments) { - req.commentCounts = comments; - next(); - }); - }); - }); -}; - -/** - * Retrieves list of commenting targets into which the current user - * has subscribed for e-mail updates. - * - * Stores them into `req.commentMeta.subscriptions` field as array: - * - * [ - * ["class", "Ext", ""], - * ["class", "Ext", "method-define"], - * ["class", "Ext.Panel", "cfg-title"] - * ] - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.getCommentSubscriptions = function(req, res, next) { - - req.commentMeta = req.commentMeta || {}; - req.commentMeta.subscriptions = req.commentMeta.subscriptions || []; - - if (req.session.user) { - Subscription.find({ - sdk: req.params.sdk, - version: req.params.version, - userId: req.session.user.userid - }, function(err, subscriptions) { - req.commentMeta.subscriptions = _.map(subscriptions, function(subscription) { - return subscription.target; - }); - next(); - }); - } else { - next(); - } -}; - -/** - * Retrieves list of comments marked 'read' by the current user. - * - * Stores them into `req.commentMeta.reads` field as array: - * - * [ - * 'abc123', - * 'abc456', - * 'abc789' - * ] - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -exports.getCommentReads = function(req, res, next) { - - req.commentMeta = req.commentMeta || {}; - req.commentMeta.reads = req.commentMeta.reads || []; - - if (req.session.user && req.session.user.moderator) { - Meta.find({ - userId: req.session.user.userid - }, function(err, commentMeta) { - req.commentMeta.reads = _.map(commentMeta, function(commentMeta) { - return commentMeta.commentId; - }); - next(); - }); - } else { - next(); - } -}; - -