jsduck 3.10.0 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
-
|