jsduck 3.10.0 → 3.10.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/Rakefile +10 -7
- data/jsduck.gemspec +7 -3
- data/lib/jsduck/doc_formatter.rb +4 -7
- data/lib/jsduck/guides.rb +5 -3
- data/lib/jsduck/html.rb +25 -0
- data/lib/jsduck/inline_examples.rb +2 -5
- data/lib/jsduck/inline_img.rb +2 -2
- data/lib/jsduck/inline_video.rb +2 -2
- data/lib/jsduck/options.rb +1 -1
- data/lib/jsduck/renderer.rb +3 -3
- data/lib/jsduck/source_file.rb +2 -2
- metadata +52 -10
- data/opt/aside.png +0 -0
- data/opt/comments-server-side/.gitignore +0 -2
- data/opt/comments-server-side/ForumUser.js +0 -80
- data/opt/comments-server-side/app.js +0 -366
- data/opt/comments-server-side/database.js +0 -53
- data/opt/comments-server-side/package.json +0 -19
- data/opt/comments-server-side/util.js +0 -396
- data/opt/example.js +0 -149
@@ -1,366 +0,0 @@
|
|
1
|
-
|
2
|
-
/**
|
3
|
-
* JSDuck authentication / commenting server side element. Requires Node.js + MongoDB.
|
4
|
-
*
|
5
|
-
* Authentication assumes a vBulletin forum database, but could easily be adapted (see ForumUser.js)
|
6
|
-
*
|
7
|
-
* Expects a config file, config.js, that looks like this:
|
8
|
-
*
|
9
|
-
* exports.db = {
|
10
|
-
* user: 'forumUsername',
|
11
|
-
* password: 'forumPassword',
|
12
|
-
* host: 'forumHost',
|
13
|
-
* dbName: 'forumDb'
|
14
|
-
* };
|
15
|
-
*
|
16
|
-
* exports.sessionSecret = 'random string for session cookie encryption';
|
17
|
-
*
|
18
|
-
* exports.mongoDb = 'mongodb://mongoHost:port/comments';
|
19
|
-
*
|
20
|
-
*/
|
21
|
-
|
22
|
-
var config = require('./config');
|
23
|
-
require('./database');
|
24
|
-
|
25
|
-
var mysql = require('mysql'),
|
26
|
-
client = mysql.createClient({
|
27
|
-
host: config.db.host,
|
28
|
-
user: config.db.user,
|
29
|
-
password: config.db.password,
|
30
|
-
database: config.db.dbName
|
31
|
-
}),
|
32
|
-
express = require('express'),
|
33
|
-
MongoStore = require('connect-mongo'),
|
34
|
-
_ = require('underscore'),
|
35
|
-
ForumUser = require('./ForumUser').ForumUser,
|
36
|
-
forumUser = new ForumUser(client),
|
37
|
-
util = require('./util'),
|
38
|
-
crypto = require('crypto'),
|
39
|
-
mongoose = require('mongoose');
|
40
|
-
|
41
|
-
var app = express();
|
42
|
-
|
43
|
-
app.configure(function() {
|
44
|
-
|
45
|
-
// Headers for Cross Origin Resource Sharing (CORS)
|
46
|
-
app.use(function (req, res, next) {
|
47
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
48
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
49
|
-
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
|
50
|
-
next();
|
51
|
-
});
|
52
|
-
|
53
|
-
app.use(express.cookieParser(config.sessionSecret));
|
54
|
-
|
55
|
-
// Hack to set session cookie if session ID is set as a URL param.
|
56
|
-
// This is because not all browsers support sending cookies via CORS
|
57
|
-
app.use(function(req, res, next) {
|
58
|
-
if (req.query.sid && req.query.sid != 'null') {
|
59
|
-
var sid = req.query.sid.replace(/ /g, '+');
|
60
|
-
req.sessionID = sid;
|
61
|
-
req.signedCookies = req.signedCookies || {};
|
62
|
-
req.signedCookies['sencha_docs'] = sid;
|
63
|
-
}
|
64
|
-
next();
|
65
|
-
});
|
66
|
-
|
67
|
-
// Use MongoDB for session storage
|
68
|
-
app.use(express.session({
|
69
|
-
secret: config.sessionSecret,
|
70
|
-
key: 'sencha_docs',
|
71
|
-
store: new MongoStore({
|
72
|
-
url: exports.mongoDb + "/sessions"
|
73
|
-
})
|
74
|
-
}));
|
75
|
-
|
76
|
-
app.use(function(req, res, next) {
|
77
|
-
// IE doesn't get content-type, so default to form encoded.
|
78
|
-
if (!req.headers['content-type']) {
|
79
|
-
req.headers['content-type'] = 'application/x-www-form-urlencoded';
|
80
|
-
}
|
81
|
-
next();
|
82
|
-
});
|
83
|
-
|
84
|
-
app.use(express.bodyParser());
|
85
|
-
app.use(express.methodOverride());
|
86
|
-
|
87
|
-
app.enable('jsonp callback');
|
88
|
-
});
|
89
|
-
|
90
|
-
app.configure('development', function(){
|
91
|
-
app.use(express.logger('dev'));
|
92
|
-
app.use(express.errorHandler());
|
93
|
-
});
|
94
|
-
|
95
|
-
/**
|
96
|
-
* Authentication
|
97
|
-
*/
|
98
|
-
|
99
|
-
app.get('/auth/session', function(req, res) {
|
100
|
-
var result = req.session && req.session.user && forumUser.clientUser(req.session.user);
|
101
|
-
res.json(result || false);
|
102
|
-
});
|
103
|
-
|
104
|
-
app.post('/auth/login', function(req, res){
|
105
|
-
|
106
|
-
forumUser.login(req.body.username, req.body.password, function(err, result) {
|
107
|
-
|
108
|
-
if (err) {
|
109
|
-
res.json({ success: false, reason: err });
|
110
|
-
return;
|
111
|
-
}
|
112
|
-
|
113
|
-
req.session = req.session || {};
|
114
|
-
req.session.user = result;
|
115
|
-
|
116
|
-
var response = _.extend(forumUser.clientUser(result), {
|
117
|
-
sessionID: req.sessionID,
|
118
|
-
success: true
|
119
|
-
});
|
120
|
-
|
121
|
-
res.json(response);
|
122
|
-
});
|
123
|
-
});
|
124
|
-
|
125
|
-
// Remove session
|
126
|
-
app.post('/auth/logout', function(req, res){
|
127
|
-
req.session.user = null;
|
128
|
-
res.json({ success: true });
|
129
|
-
});
|
130
|
-
|
131
|
-
/**
|
132
|
-
* Handles comment unsubscription requests.
|
133
|
-
*/
|
134
|
-
app.get('/auth/unsubscribe/:subscriptionId', function(req, res) {
|
135
|
-
|
136
|
-
Subscription.findOne({ _id: req.params.subscriptionId }, function(err, subscription) {
|
137
|
-
if (err) throw(err);
|
138
|
-
|
139
|
-
if (subscription) {
|
140
|
-
if (req.query.all == 'true') {
|
141
|
-
Subscription.remove({ userId: subscription.userId }, function(err) {
|
142
|
-
res.send("You have been unsubscribed from all threads.");
|
143
|
-
});
|
144
|
-
} else {
|
145
|
-
Subscription.remove({ _id: req.params.subscriptionId }, function(err) {
|
146
|
-
res.send("You have been unsubscribed from that thread.");
|
147
|
-
});
|
148
|
-
}
|
149
|
-
} else {
|
150
|
-
res.send("You are already unsubscribed.");
|
151
|
-
}
|
152
|
-
});
|
153
|
-
});
|
154
|
-
|
155
|
-
|
156
|
-
/**
|
157
|
-
* Commenting
|
158
|
-
*/
|
159
|
-
|
160
|
-
/**
|
161
|
-
* Returns a list of comments for a particular target (eg class, guide, video)
|
162
|
-
*/
|
163
|
-
app.get('/auth/:sdk/:version/comments', util.getCommentReads, function(req, res) {
|
164
|
-
|
165
|
-
if (!req.query.startkey) {
|
166
|
-
res.json({error: 'Invalid request'});
|
167
|
-
return;
|
168
|
-
}
|
169
|
-
|
170
|
-
Comment.find({
|
171
|
-
target: JSON.parse(req.query.startkey),
|
172
|
-
deleted: { '$ne': true },
|
173
|
-
sdk: req.params.sdk,
|
174
|
-
version: req.params.version
|
175
|
-
}).sort('createdAt', 1).run(function(err, comments){
|
176
|
-
res.json(util.scoreComments(comments, req));
|
177
|
-
});
|
178
|
-
});
|
179
|
-
|
180
|
-
/**
|
181
|
-
* Returns n most recent comments.
|
182
|
-
* Takes two parameters: offset and limit.
|
183
|
-
*
|
184
|
-
* The last comment object returned will contain `total_rows`,
|
185
|
-
* `offset` and `limit` fields. I'd say it's a hack, but at least
|
186
|
-
* it works for now.
|
187
|
-
*/
|
188
|
-
app.get('/auth/:sdk/:version/comments_recent', util.getCommentReads, function(req, res) {
|
189
|
-
var offset = parseInt(req.query.offset, 10) || 0;
|
190
|
-
var limit = parseInt(req.query.limit, 10) || 100;
|
191
|
-
var filter = {
|
192
|
-
deleted: { '$ne': true },
|
193
|
-
sdk: req.params.sdk,
|
194
|
-
version: req.params.version
|
195
|
-
};
|
196
|
-
|
197
|
-
if (req.query.hideRead && req.commentMeta.reads.length > 0) {
|
198
|
-
filter._id = { $nin: req.commentMeta.reads };
|
199
|
-
}
|
200
|
-
|
201
|
-
Comment.find(filter).sort('createdAt', -1).skip(offset).limit(limit).run(function(err, comments) {
|
202
|
-
comments = util.scoreComments(comments, req);
|
203
|
-
// Count all comments, store count to last comment
|
204
|
-
Comment.count(filter).run(function(err, count) {
|
205
|
-
var last = comments[comments.length-1];
|
206
|
-
last.total_rows = count;
|
207
|
-
last.offset = offset;
|
208
|
-
last.limit = limit;
|
209
|
-
res.json(comments);
|
210
|
-
});
|
211
|
-
});
|
212
|
-
});
|
213
|
-
|
214
|
-
/**
|
215
|
-
* Returns number of comments for each class/member,
|
216
|
-
* and a list of classes/members into which the user has subscribed.
|
217
|
-
*/
|
218
|
-
app.get('/auth/:sdk/:version/comments_meta', util.getCommentCounts, util.getCommentSubscriptions, function(req, res) {
|
219
|
-
res.send({ comments: req.commentCounts, subscriptions: req.commentSubscriptions || [] });
|
220
|
-
});
|
221
|
-
|
222
|
-
/**
|
223
|
-
* Returns an individual comment (used when editing a comment)
|
224
|
-
*/
|
225
|
-
app.get('/auth/:sdk/:version/comments/:commentId', util.findComment, function(req, res) {
|
226
|
-
res.json({ success: true, content: req.comment.content });
|
227
|
-
});
|
228
|
-
|
229
|
-
/**
|
230
|
-
* Creates a new comment
|
231
|
-
*/
|
232
|
-
app.post('/auth/:sdk/:version/comments', util.requireLoggedInUser, function(req, res) {
|
233
|
-
|
234
|
-
var target = JSON.parse(req.body.target);
|
235
|
-
|
236
|
-
if (target.length === 2) {
|
237
|
-
target.push('');
|
238
|
-
}
|
239
|
-
|
240
|
-
var comment = new Comment({
|
241
|
-
author: req.session.user.username,
|
242
|
-
userId: req.session.user.userid,
|
243
|
-
content: req.body.comment,
|
244
|
-
action: req.body.action,
|
245
|
-
rating: Number(req.body.rating),
|
246
|
-
contentHtml: util.markdown(req.body.comment),
|
247
|
-
downVotes: [],
|
248
|
-
upVotes: [],
|
249
|
-
createdAt: new Date,
|
250
|
-
target: target,
|
251
|
-
emailHash: crypto.createHash('md5').update(req.session.user.email).digest("hex"),
|
252
|
-
sdk: req.params.sdk,
|
253
|
-
version: req.params.version,
|
254
|
-
moderator: req.session.user.moderator,
|
255
|
-
title: req.body.title,
|
256
|
-
url: req.body.url
|
257
|
-
});
|
258
|
-
|
259
|
-
comment.save(function(err, response) {
|
260
|
-
res.json({ success: true, id: response._id, action: req.body.action });
|
261
|
-
|
262
|
-
util.sendEmailUpdates(comment);
|
263
|
-
});
|
264
|
-
});
|
265
|
-
|
266
|
-
/**
|
267
|
-
* Updates an existing comment (for voting or updating contents)
|
268
|
-
*/
|
269
|
-
app.post('/auth/:sdk/:version/comments/:commentId', util.requireLoggedInUser, util.findComment, function(req, res) {
|
270
|
-
|
271
|
-
var voteDirection,
|
272
|
-
comment = req.comment;
|
273
|
-
|
274
|
-
if (req.body.vote) {
|
275
|
-
util.vote(req, res, comment);
|
276
|
-
} else {
|
277
|
-
util.requireOwner(req, res, function() {
|
278
|
-
comment.content = req.body.content;
|
279
|
-
comment.contentHtml = util.markdown(req.body.content);
|
280
|
-
|
281
|
-
comment.updates = comment.updates || [];
|
282
|
-
comment.updates.push({
|
283
|
-
updatedAt: String(new Date()),
|
284
|
-
author: req.session.user.username
|
285
|
-
});
|
286
|
-
|
287
|
-
comment.save(function(err, response) {
|
288
|
-
res.json({ success: true, content: comment.contentHtml });
|
289
|
-
});
|
290
|
-
});
|
291
|
-
}
|
292
|
-
});
|
293
|
-
|
294
|
-
/**
|
295
|
-
* Deletes a comment
|
296
|
-
*/
|
297
|
-
app.post('/auth/:sdk/:version/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) {
|
298
|
-
req.comment.deleted = true;
|
299
|
-
req.comment.save(function(err, response) {
|
300
|
-
res.send({ success: true });
|
301
|
-
});
|
302
|
-
});
|
303
|
-
|
304
|
-
/**
|
305
|
-
* Restores deleted comment
|
306
|
-
*/
|
307
|
-
app.post('/auth/:sdk/:version/comments/:commentId/undo_delete', util.requireLoggedInUser, util.findComment, util.requireOwner, util.getCommentReads, function(req, res) {
|
308
|
-
req.comment.deleted = false;
|
309
|
-
req.comment.save(function(err, response) {
|
310
|
-
res.send({ success: true, comment: util.scoreComments([req.comment], req)[0] });
|
311
|
-
});
|
312
|
-
});
|
313
|
-
|
314
|
-
/**
|
315
|
-
* Marks a comment 'read'
|
316
|
-
*/
|
317
|
-
app.post('/auth/:sdk/:version/comments/:commentId/read', util.requireLoggedInUser, util.findCommentMeta, function(req, res) {
|
318
|
-
req.commentMeta.metaType = 'read';
|
319
|
-
req.commentMeta.save(function(err, response) {
|
320
|
-
res.send({ success: true });
|
321
|
-
});
|
322
|
-
});
|
323
|
-
|
324
|
-
/**
|
325
|
-
* Get email subscriptions
|
326
|
-
*/
|
327
|
-
app.get('/auth/:sdk/:version/subscriptions', util.getCommentSubscriptions, function(req, res) {
|
328
|
-
res.json({ subscriptions: req.commentMeta.subscriptions });
|
329
|
-
});
|
330
|
-
|
331
|
-
/**
|
332
|
-
* Subscibe / unsubscribe to a comment thread
|
333
|
-
*/
|
334
|
-
app.post('/auth/:sdk/:version/subscribe', util.requireLoggedInUser, function(req, res) {
|
335
|
-
|
336
|
-
var subscriptionBody = {
|
337
|
-
sdk: req.params.sdk,
|
338
|
-
version: req.params.version,
|
339
|
-
target: JSON.parse(req.body.target),
|
340
|
-
userId: req.session.user.userid
|
341
|
-
};
|
342
|
-
|
343
|
-
Subscription.findOne(subscriptionBody, function(err, subscription) {
|
344
|
-
|
345
|
-
if (subscription && req.body.subscribed == 'false') {
|
346
|
-
|
347
|
-
subscription.remove(function(err, ok) {
|
348
|
-
res.send({ success: true });
|
349
|
-
});
|
350
|
-
|
351
|
-
} else if (!subscription && req.body.subscribed == 'true') {
|
352
|
-
|
353
|
-
subscription = new Subscription(subscriptionBody);
|
354
|
-
subscription.email = req.session.user.email;
|
355
|
-
|
356
|
-
subscription.save(function(err, ok) {
|
357
|
-
res.send({ success: true });
|
358
|
-
});
|
359
|
-
}
|
360
|
-
});
|
361
|
-
});
|
362
|
-
|
363
|
-
var port = 3000;
|
364
|
-
app.listen(port);
|
365
|
-
console.log("Server started at port "+port+"...");
|
366
|
-
|
@@ -1,53 +0,0 @@
|
|
1
|
-
|
2
|
-
/**
|
3
|
-
* Defines comment schema and connects to database
|
4
|
-
*/
|
5
|
-
|
6
|
-
var mongoose = require('mongoose'),
|
7
|
-
config = require('./config');
|
8
|
-
|
9
|
-
Comment = mongoose.model('Comment', new mongoose.Schema({
|
10
|
-
sdk: String,
|
11
|
-
version: String,
|
12
|
-
|
13
|
-
action: String,
|
14
|
-
author: String,
|
15
|
-
userId: Number,
|
16
|
-
content: String,
|
17
|
-
contentHtml: String,
|
18
|
-
createdAt: Date,
|
19
|
-
downVotes: Array,
|
20
|
-
emailHash: String,
|
21
|
-
rating: Number,
|
22
|
-
target: Array,
|
23
|
-
upVotes: Array,
|
24
|
-
deleted: Boolean,
|
25
|
-
updates: Array,
|
26
|
-
mod: Boolean,
|
27
|
-
title: String,
|
28
|
-
url: String
|
29
|
-
}));
|
30
|
-
|
31
|
-
Subscription = mongoose.model('Subscription', new mongoose.Schema({
|
32
|
-
sdk: String,
|
33
|
-
version: String,
|
34
|
-
|
35
|
-
createdAt: Date,
|
36
|
-
userId: Number,
|
37
|
-
email: String,
|
38
|
-
target: Array
|
39
|
-
}));
|
40
|
-
|
41
|
-
Meta = mongoose.model('Meta', new mongoose.Schema({
|
42
|
-
sdk: String,
|
43
|
-
version: String,
|
44
|
-
|
45
|
-
createdAt: Date,
|
46
|
-
userId: Number,
|
47
|
-
commentId: String,
|
48
|
-
metaType: String
|
49
|
-
}));
|
50
|
-
|
51
|
-
mongoose.connect(config.mongoDb, function(err, ok) {
|
52
|
-
console.log("Connected to DB")
|
53
|
-
});
|
@@ -1,19 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"name": "jsduck_comments",
|
3
|
-
"version": "0.5.0",
|
4
|
-
"description": "Commenting backend for JSDuck Documentation",
|
5
|
-
"author": "Nick Poudlen <nick@sencha.com>",
|
6
|
-
"dependencies": {
|
7
|
-
"express": "git://github.com/visionmedia/express.git",
|
8
|
-
"express-namespace": "",
|
9
|
-
"connect": "",
|
10
|
-
"connect-mongo": "",
|
11
|
-
"marked": "",
|
12
|
-
"mongoose": "",
|
13
|
-
"mysql": "",
|
14
|
-
"sanitizer": "",
|
15
|
-
"step": "",
|
16
|
-
"underscore": "",
|
17
|
-
"nodemailer": ""
|
18
|
-
}
|
19
|
-
}
|
@@ -1,396 +0,0 @@
|
|
1
|
-
|
2
|
-
var marked = require('marked'),
|
3
|
-
_ = require('underscore'),
|
4
|
-
sanitizer = require('sanitizer'),
|
5
|
-
nodemailer = require("nodemailer"),
|
6
|
-
mongoose = require('mongoose');
|
7
|
-
|
8
|
-
/**
|
9
|
-
* Converts Markdown-formatted comment text into HTML.
|
10
|
-
*
|
11
|
-
* @param {String} content Markdown-formatted text
|
12
|
-
* @return {String} HTML
|
13
|
-
*/
|
14
|
-
exports.markdown = function(content) {
|
15
|
-
var markdowned;
|
16
|
-
try {
|
17
|
-
markdowned = marked(content);
|
18
|
-
} catch(e) {
|
19
|
-
markdowned = content;
|
20
|
-
}
|
21
|
-
|
22
|
-
// Strip dangerous markup, but allow links to all URL-s
|
23
|
-
var sanitized_output = sanitizer.sanitize(markdowned, function(str) {
|
24
|
-
return str;
|
25
|
-
});
|
26
|
-
|
27
|
-
// IE does not support '
|
28
|
-
return sanitized_output.replace(/'/g, ''');
|
29
|
-
};
|
30
|
-
|
31
|
-
/**
|
32
|
-
* Calculates up/down scores for each comment.
|
33
|
-
*
|
34
|
-
* Marks if the current user has already voted on the comment.
|
35
|
-
* Ensures createdAt timestamp is a string.
|
36
|
-
*
|
37
|
-
* @param {Object[]} comments
|
38
|
-
* @param {Object} req Containing username data
|
39
|
-
* @return {Object[]}
|
40
|
-
*/
|
41
|
-
exports.scoreComments = function(comments, req) {
|
42
|
-
return _.map(comments, function(comment) {
|
43
|
-
comment = _.extend(comment._doc, {
|
44
|
-
score: comment.upVotes.length - comment.downVotes.length,
|
45
|
-
createdAt: String(comment.createdAt)
|
46
|
-
});
|
47
|
-
|
48
|
-
if (req.commentMeta.reads.length > 0) {
|
49
|
-
comment.read = _.include(req.commentMeta.reads, ""+comment._id);
|
50
|
-
}
|
51
|
-
|
52
|
-
if (req.session.user) {
|
53
|
-
comment.upVote = _.contains(comment.upVotes, req.session.user.username);
|
54
|
-
comment.downVote = _.contains(comment.downVotes, req.session.user.username);
|
55
|
-
}
|
56
|
-
|
57
|
-
return comment;
|
58
|
-
});
|
59
|
-
};
|
60
|
-
|
61
|
-
/**
|
62
|
-
* Performs voting on comment.
|
63
|
-
*
|
64
|
-
* @param {Object} req The request object.
|
65
|
-
* @param {Object} res The response object where voting result is written.
|
66
|
-
* @param {Comment} comment The comment to vote on.
|
67
|
-
*/
|
68
|
-
exports.vote = function(req, res, comment) {
|
69
|
-
var voteDirection;
|
70
|
-
var username = req.session.user.username;
|
71
|
-
|
72
|
-
if (username == comment.author) {
|
73
|
-
|
74
|
-
// Ignore votes from the author
|
75
|
-
res.json({success: false, reason: 'You cannot vote on your own content'});
|
76
|
-
return;
|
77
|
-
|
78
|
-
} else if (req.body.vote == 'up' && !_.include(comment.upVotes, username)) {
|
79
|
-
|
80
|
-
var voted = _.include(comment.downVotes, username);
|
81
|
-
|
82
|
-
comment.downVotes = _.reject(comment.downVotes, function(v) {
|
83
|
-
return v == username;
|
84
|
-
});
|
85
|
-
|
86
|
-
if (!voted) {
|
87
|
-
voteDirection = 'up';
|
88
|
-
comment.upVotes.push(username);
|
89
|
-
}
|
90
|
-
} else if (req.body.vote == 'down' && !_.include(comment.downVotes, username)) {
|
91
|
-
|
92
|
-
var voted = _.include(comment.upVotes, username);
|
93
|
-
|
94
|
-
comment.upVotes = _.reject(comment.upVotes, function(v) {
|
95
|
-
return v == username;
|
96
|
-
});
|
97
|
-
|
98
|
-
if (!voted) {
|
99
|
-
voteDirection = 'down';
|
100
|
-
comment.downVotes.push(username);
|
101
|
-
}
|
102
|
-
}
|
103
|
-
|
104
|
-
comment.save(function(err, response) {
|
105
|
-
res.json({
|
106
|
-
success: true,
|
107
|
-
direction: voteDirection,
|
108
|
-
total: (comment.upVotes.length - comment.downVotes.length)
|
109
|
-
});
|
110
|
-
});
|
111
|
-
};
|
112
|
-
|
113
|
-
/**
|
114
|
-
* Ensures that user is logged in.
|
115
|
-
*
|
116
|
-
* @param {Object} req
|
117
|
-
* @param {Object} res
|
118
|
-
* @param {Function} next
|
119
|
-
*/
|
120
|
-
exports.requireLoggedInUser = function(req, res, next) {
|
121
|
-
if (!req.session || !req.session.user) {
|
122
|
-
res.json({success: false, reason: 'Forbidden'}, 403);
|
123
|
-
} else {
|
124
|
-
next();
|
125
|
-
}
|
126
|
-
};
|
127
|
-
|
128
|
-
/**
|
129
|
-
* Looks up comment by ID.
|
130
|
-
*
|
131
|
-
* Stores it into `req.comment`.
|
132
|
-
*
|
133
|
-
* @param {Object} req
|
134
|
-
* @param {Object} res
|
135
|
-
* @param {Function} next
|
136
|
-
*/
|
137
|
-
exports.findComment = function(req, res, next) {
|
138
|
-
if (req.params.commentId) {
|
139
|
-
Comment.findById(req.params.commentId, function(err, comment) {
|
140
|
-
req.comment = comment;
|
141
|
-
next();
|
142
|
-
});
|
143
|
-
} else {
|
144
|
-
res.json({success: false, reason: 'No such comment'});
|
145
|
-
}
|
146
|
-
};
|
147
|
-
|
148
|
-
/**
|
149
|
-
* Looks up comment meta by comment ID.
|
150
|
-
*
|
151
|
-
* Stores it into `req.commentMeta`.
|
152
|
-
*
|
153
|
-
* @param {Object} req
|
154
|
-
* @param {Object} res
|
155
|
-
* @param {Function} next
|
156
|
-
*/
|
157
|
-
exports.findCommentMeta = function(req, res, next) {
|
158
|
-
if (req.params.commentId) {
|
159
|
-
|
160
|
-
var userCommentMeta = {
|
161
|
-
userId: req.session.user.userid,
|
162
|
-
commentId: req.params.commentId
|
163
|
-
};
|
164
|
-
|
165
|
-
Meta.findOne(userCommentMeta, function(err, commentMeta) {
|
166
|
-
req.commentMeta = commentMeta || new Meta(userCommentMeta);
|
167
|
-
next();
|
168
|
-
});
|
169
|
-
} else {
|
170
|
-
res.json({success: false, reason: 'No such comment'});
|
171
|
-
}
|
172
|
-
};
|
173
|
-
|
174
|
-
// True if the user is moderator
|
175
|
-
function isModerator(user) {
|
176
|
-
return _.include(user.membergroupids, 7);
|
177
|
-
}
|
178
|
-
|
179
|
-
// True if the user is author of the comment
|
180
|
-
function isAuthor(user, comment) {
|
181
|
-
return user.username === comment.author;
|
182
|
-
}
|
183
|
-
|
184
|
-
/**
|
185
|
-
* Ensures that user is allowed to modify/delete the comment,
|
186
|
-
* that is, he is the owner of the comment or a moderator.
|
187
|
-
*
|
188
|
-
* @param {Object} req
|
189
|
-
* @param {Object} res
|
190
|
-
* @param {Function} next
|
191
|
-
*/
|
192
|
-
exports.requireOwner = function(req, res, next) {
|
193
|
-
if (isModerator(req.session.user) || isAuthor(req.session.user, req.comment)) {
|
194
|
-
next();
|
195
|
-
}
|
196
|
-
else {
|
197
|
-
res.json({ success: false, reason: 'Forbidden' }, 403);
|
198
|
-
}
|
199
|
-
};
|
200
|
-
|
201
|
-
/**
|
202
|
-
* Sends e-mail updates when comment is posted to a thread that has
|
203
|
-
* subscribers.
|
204
|
-
*
|
205
|
-
* @param {Comment} comment
|
206
|
-
*/
|
207
|
-
exports.sendEmailUpdates = function(comment) {
|
208
|
-
var mailTransport = nodemailer.createTransport("SMTP",{
|
209
|
-
host: 'localhost',
|
210
|
-
port: 25
|
211
|
-
});
|
212
|
-
|
213
|
-
var sendSubscriptionEmail = function(emails) {
|
214
|
-
var email = emails.shift();
|
215
|
-
|
216
|
-
if (email) {
|
217
|
-
nodemailer.sendMail(email, function(err){
|
218
|
-
if (err){
|
219
|
-
console.log(err);
|
220
|
-
} else{
|
221
|
-
console.log("Sent email to " + email.to);
|
222
|
-
sendSubscriptionEmail(emails);
|
223
|
-
}
|
224
|
-
});
|
225
|
-
} else {
|
226
|
-
console.log("Finished sending emails");
|
227
|
-
mailTransport.close();
|
228
|
-
}
|
229
|
-
};
|
230
|
-
|
231
|
-
var subscriptionBody = {
|
232
|
-
sdk: comment.sdk,
|
233
|
-
version: comment.version,
|
234
|
-
target: comment.target
|
235
|
-
};
|
236
|
-
|
237
|
-
var emails = [];
|
238
|
-
|
239
|
-
Subscription.find(subscriptionBody, function(err, subscriptions) {
|
240
|
-
_.each(subscriptions, function(subscription) {
|
241
|
-
var mailOptions = {
|
242
|
-
transport: mailTransport,
|
243
|
-
from: "Sencha Documentation <no-reply@sencha.com>",
|
244
|
-
to: subscription.email,
|
245
|
-
subject: "Comment on '" + comment.title + "'",
|
246
|
-
text: [
|
247
|
-
"A comment by " + comment.author + " on '" + comment.title + "' was posted on the Sencha Documentation:\n",
|
248
|
-
comment.content + "\n",
|
249
|
-
"--",
|
250
|
-
"Original thread: " + comment.url,
|
251
|
-
"Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id,
|
252
|
-
"Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true'
|
253
|
-
].join("\n")
|
254
|
-
};
|
255
|
-
|
256
|
-
if (Number(comment.userId) != Number(subscription.userId)) {
|
257
|
-
emails.push(mailOptions);
|
258
|
-
}
|
259
|
-
});
|
260
|
-
|
261
|
-
if (emails.length) {
|
262
|
-
sendSubscriptionEmail(emails);
|
263
|
-
} else {
|
264
|
-
console.log("No emails to send");
|
265
|
-
}
|
266
|
-
});
|
267
|
-
};
|
268
|
-
|
269
|
-
/**
|
270
|
-
* Retrieves comment counts for each target.
|
271
|
-
*
|
272
|
-
* Stores into `req.commentCounts` field an array like this:
|
273
|
-
*
|
274
|
-
* [
|
275
|
-
* {"_id": "class__Ext__", "value": 3},
|
276
|
-
* {"_id": "class__Ext__method-define", "value": 1},
|
277
|
-
* {"_id": "class__Ext.Panel__cfg-title", "value": 8}
|
278
|
-
* ]
|
279
|
-
*
|
280
|
-
* @param {Object} req
|
281
|
-
* @param {Object} res
|
282
|
-
* @param {Function} next
|
283
|
-
*/
|
284
|
-
exports.getCommentCounts = function(req, res, next) {
|
285
|
-
// Map each comment into: ("type__Class__member", 1)
|
286
|
-
var map = function() {
|
287
|
-
if (this.target) {
|
288
|
-
emit(this.target.slice(0,3).join('__'), 1);
|
289
|
-
} else {
|
290
|
-
return;
|
291
|
-
}
|
292
|
-
};
|
293
|
-
|
294
|
-
// Sum comment counts for each target
|
295
|
-
var reduce = function(key, values) {
|
296
|
-
var total = 0;
|
297
|
-
|
298
|
-
for (var i = 0; i < values.length; i++) {
|
299
|
-
total += values[i];
|
300
|
-
}
|
301
|
-
|
302
|
-
return total;
|
303
|
-
};
|
304
|
-
|
305
|
-
mongoose.connection.db.executeDbCommand({
|
306
|
-
mapreduce: 'comments',
|
307
|
-
map: map.toString(),
|
308
|
-
reduce: reduce.toString(),
|
309
|
-
out: 'commentCounts',
|
310
|
-
query: {
|
311
|
-
deleted: { '$ne': true },
|
312
|
-
sdk: req.params.sdk,
|
313
|
-
version: req.params.version
|
314
|
-
}
|
315
|
-
}, function(err, dbres) {
|
316
|
-
mongoose.connection.db.collection('commentCounts', function(err, collection) {
|
317
|
-
collection.find({}).toArray(function(err, comments) {
|
318
|
-
req.commentCounts = comments;
|
319
|
-
next();
|
320
|
-
});
|
321
|
-
});
|
322
|
-
});
|
323
|
-
};
|
324
|
-
|
325
|
-
/**
|
326
|
-
* Retrieves list of commenting targets into which the current user
|
327
|
-
* has subscribed for e-mail updates.
|
328
|
-
*
|
329
|
-
* Stores them into `req.commentMeta.subscriptions` field as array:
|
330
|
-
*
|
331
|
-
* [
|
332
|
-
* ["class", "Ext", ""],
|
333
|
-
* ["class", "Ext", "method-define"],
|
334
|
-
* ["class", "Ext.Panel", "cfg-title"]
|
335
|
-
* ]
|
336
|
-
*
|
337
|
-
* @param {Object} req
|
338
|
-
* @param {Object} res
|
339
|
-
* @param {Function} next
|
340
|
-
*/
|
341
|
-
exports.getCommentSubscriptions = function(req, res, next) {
|
342
|
-
|
343
|
-
req.commentMeta = req.commentMeta || {};
|
344
|
-
req.commentMeta.subscriptions = req.commentMeta.subscriptions || [];
|
345
|
-
|
346
|
-
if (req.session.user) {
|
347
|
-
Subscription.find({
|
348
|
-
sdk: req.params.sdk,
|
349
|
-
version: req.params.version,
|
350
|
-
userId: req.session.user.userid
|
351
|
-
}, function(err, subscriptions) {
|
352
|
-
req.commentMeta.subscriptions = _.map(subscriptions, function(subscription) {
|
353
|
-
return subscription.target;
|
354
|
-
});
|
355
|
-
next();
|
356
|
-
});
|
357
|
-
} else {
|
358
|
-
next();
|
359
|
-
}
|
360
|
-
};
|
361
|
-
|
362
|
-
/**
|
363
|
-
* Retrieves list of comments marked 'read' by the current user.
|
364
|
-
*
|
365
|
-
* Stores them into `req.commentMeta.reads` field as array:
|
366
|
-
*
|
367
|
-
* [
|
368
|
-
* 'abc123',
|
369
|
-
* 'abc456',
|
370
|
-
* 'abc789'
|
371
|
-
* ]
|
372
|
-
*
|
373
|
-
* @param {Object} req
|
374
|
-
* @param {Object} res
|
375
|
-
* @param {Function} next
|
376
|
-
*/
|
377
|
-
exports.getCommentReads = function(req, res, next) {
|
378
|
-
|
379
|
-
req.commentMeta = req.commentMeta || {};
|
380
|
-
req.commentMeta.reads = req.commentMeta.reads || [];
|
381
|
-
|
382
|
-
if (req.session.user && isModerator(req.session.user)) {
|
383
|
-
Meta.find({
|
384
|
-
userId: req.session.user.userid
|
385
|
-
}, function(err, commentMeta) {
|
386
|
-
req.commentMeta.reads = _.map(commentMeta, function(commentMeta) {
|
387
|
-
return commentMeta.commentId;
|
388
|
-
});
|
389
|
-
next();
|
390
|
-
});
|
391
|
-
} else {
|
392
|
-
next();
|
393
|
-
}
|
394
|
-
};
|
395
|
-
|
396
|
-
|