jsduck 3.7.0 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
1
  require 'jsduck/null_object'
2
+ require 'jsduck/io'
2
3
 
3
4
  module JsDuck
4
5
 
@@ -14,7 +15,7 @@ module JsDuck
14
15
 
15
16
  # Parses welcome HTML file with content for welcome page.
16
17
  def initialize(filename)
17
- @html = IO.read(filename)
18
+ @html = JsDuck::IO.read(filename)
18
19
  end
19
20
 
20
21
  # Returns the HTML
@@ -138,7 +138,7 @@ app.namespace('/auth', function(){
138
138
  });
139
139
  }
140
140
  } else {
141
- res.send("You are already unsubscribed.")
141
+ res.send("You are already unsubscribed.");
142
142
  }
143
143
  });
144
144
  });
@@ -156,34 +156,56 @@ app.namespace('/auth/:sdk/:version', function(){
156
156
  */
157
157
  app.get('/comments', function(req, res) {
158
158
 
159
+ if (!req.query.startkey) {
160
+ res.json({error: 'Invalid request'});
161
+ return;
162
+ }
163
+
159
164
  Comment.find({
160
165
  target: JSON.parse(req.query.startkey),
161
166
  deleted: { '$ne': true },
162
167
  sdk: req.params.sdk,
163
168
  version: req.params.version
164
169
  }).sort('createdAt', 1).run(function(err, comments){
165
- res.json(util.formatComments(comments, req));
170
+ res.json(util.scoreComments(comments, req));
166
171
  });
167
172
  });
168
173
 
169
174
  /**
170
- * Returns 100 most recent comments.
175
+ * Returns n most recent comments.
176
+ * Takes two parameters: offset and limit.
177
+ *
178
+ * The last comment object returned will contain `total_rows`,
179
+ * `offset` and `limit` fields. I'd say it's a hack, but at least
180
+ * it works for now.
171
181
  */
172
182
  app.get('/comments_recent', function(req, res) {
173
- Comment.find({
183
+ var offset = parseInt(req.query.offset, 10) || 0;
184
+ var limit = parseInt(req.query.limit, 10) || 100;
185
+ var filter = {
174
186
  deleted: { '$ne': true },
175
187
  sdk: req.params.sdk,
176
188
  version: req.params.version
177
- }).sort('createdAt', -1).limit(100).run(function(err, comments){
178
- res.json(util.formatComments(comments, req));
189
+ };
190
+ Comment.find(filter).sort('createdAt', -1).skip(offset).limit(limit).run(function(err, comments) {
191
+ comments = util.scoreComments(comments, req);
192
+ // Count all comments, store count to last comment
193
+ Comment.count(filter).run(function(err, count) {
194
+ var last = comments[comments.length-1];
195
+ last.total_rows = count;
196
+ last.offset = offset;
197
+ last.limit = limit;
198
+ res.json(comments);
199
+ });
179
200
  });
180
201
  });
181
202
 
182
203
  /**
183
- * Returns number of comments for each class / method
204
+ * Returns number of comments for each class/member,
205
+ * and a list of classes/members into which the user has subscribed.
184
206
  */
185
- app.get('/comments_meta', util.getCommentsMeta, util.getCommentSubscriptions, function(req, res) {
186
- res.send({ comments: req.commentsMeta, subscriptions: req.commentSubscriptions || [] });
207
+ app.get('/comments_meta', util.getCommentCounts, util.getCommentSubscriptions, function(req, res) {
208
+ res.send({ comments: req.commentCounts, subscriptions: req.commentSubscriptions || [] });
187
209
  });
188
210
 
189
211
  /**
@@ -210,7 +232,7 @@ app.namespace('/auth/:sdk/:version', function(){
210
232
  content: req.body.comment,
211
233
  action: req.body.action,
212
234
  rating: Number(req.body.rating),
213
- contentHtml: util.sanitize(req.body.comment),
235
+ contentHtml: util.markdown(req.body.comment),
214
236
  downVotes: [],
215
237
  upVotes: [],
216
238
  createdAt: new Date,
@@ -241,24 +263,19 @@ app.namespace('/auth/:sdk/:version', function(){
241
263
  if (req.body.vote) {
242
264
  util.vote(req, res, comment);
243
265
  } else {
244
- var canUpdate = _.include(req.session.user.membergroupids, 7) || req.session.user.username == comment.author;
245
-
246
- if (!canUpdate) {
247
- res.json({success: false, reason: 'Forbidden'}, 403);
248
- return;
249
- }
250
-
251
- comment.content = req.body.content;
252
- comment.contentHtml = util.sanitize(req.body.content);
253
-
254
- comment.updates = comment.updates || [];
255
- comment.updates.push({
256
- updatedAt: String(new Date()),
257
- author: req.session.user.username
258
- });
266
+ util.requireOwner(req, res, function() {
267
+ comment.content = req.body.content;
268
+ comment.contentHtml = util.markdown(req.body.content);
269
+
270
+ comment.updates = comment.updates || [];
271
+ comment.updates.push({
272
+ updatedAt: String(new Date()),
273
+ author: req.session.user.username
274
+ });
259
275
 
260
- comment.save(function(err, response) {
261
- res.json({ success: true, content: comment.contentHtml });
276
+ comment.save(function(err, response) {
277
+ res.json({ success: true, content: comment.contentHtml });
278
+ });
262
279
  });
263
280
  }
264
281
  });
@@ -266,25 +283,23 @@ app.namespace('/auth/:sdk/:version', function(){
266
283
  /**
267
284
  * Deletes a comment
268
285
  */
269
- app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, function(req, res) {
270
-
271
- var canDelete = false,
272
- comment = req.comment;
273
-
274
- canDelete = _.include(req.session.user.membergroupids, 7) || req.session.user.username == req.comment.author;
275
-
276
- if (!canDelete) {
277
- res.json({ success: false, reason: 'Forbidden' }, 403);
278
- return;
279
- }
280
-
281
- comment.deleted = true;
282
-
283
- comment.save(function(err, response) {
286
+ app.post('/comments/:commentId/delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) {
287
+ req.comment.deleted = true;
288
+ req.comment.save(function(err, response) {
284
289
  res.send({ success: true });
285
290
  });
286
291
  });
287
292
 
293
+ /**
294
+ * Restores deleted comment
295
+ */
296
+ app.post('/comments/:commentId/undo_delete', util.requireLoggedInUser, util.findComment, util.requireOwner, function(req, res) {
297
+ req.comment.deleted = false;
298
+ req.comment.save(function(err, response) {
299
+ res.send({ success: true, comment: util.scoreComments([req.comment], req)[0] });
300
+ });
301
+ });
302
+
288
303
  /**
289
304
  * Get email subscriptions
290
305
  */
@@ -326,7 +341,7 @@ app.namespace('/auth/:sdk/:version', function(){
326
341
 
327
342
  });
328
343
 
329
-
330
- app.listen(3000);
331
- console.log("Server started...");
344
+ var port = 3000;
345
+ app.listen(port);
346
+ console.log("Server started at port "+port+"...");
332
347
 
@@ -4,7 +4,7 @@
4
4
  "description": "Commenting backend for JSDuck Documentation",
5
5
  "author": "Nick Poudlen <nick@sencha.com>",
6
6
  "dependencies": {
7
- "connect": "",
7
+ "connect": "1.8.5",
8
8
  "connect-session-mongo": "",
9
9
  "express": "",
10
10
  "express-namespace": "",
@@ -4,39 +4,41 @@ var marked = require('marked'),
4
4
  sanitizer = require('sanitizer'),
5
5
  nodemailer = require("nodemailer");
6
6
 
7
- exports.sanitize = function(content, opts) {
8
-
9
- var markdowned, sanitized_output, urlFunc;
10
-
7
+ /**
8
+ * Converts Markdown-formatted comment text into HTML.
9
+ *
10
+ * @param {String} content Markdown-formatted text
11
+ * @return {String} HTML
12
+ */
13
+ exports.markdown = function(content) {
14
+ var markdowned;
11
15
  try {
12
16
  markdowned = marked(content);
13
17
  } catch(e) {
14
18
  markdowned = content;
15
19
  }
16
20
 
17
- var exp = /(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/igm;
18
- markdowned = markdowned.replace(exp, "<a href='$1'>$1</a>");
19
-
20
- if (opts && opts.stripUrls) {
21
- urlFunc = function(str) {
22
- if (str.match(/^(http:\/\/(www\.)?sencha.com|#))/)) {
23
- return str;
24
- } else {
25
- return '';
26
- }
27
- };
28
- }
29
-
30
- sanitized_output = sanitizer.sanitize(markdowned, urlFunc);
31
- sanitized_output = sanitized_output.replace(/&apos;/g, '&#39;');
21
+ // Strip dangerous markup, but allow links to all URL-s
22
+ var sanitized_output = sanitizer.sanitize(markdowned, function(str) {
23
+ return str;
24
+ });
32
25
 
33
- return sanitized_output;
26
+ // IE does not support &apos;
27
+ return sanitized_output.replace(/&apos;/g, '&#39;');
34
28
  };
35
29
 
36
- exports.formatComments = function(comments, req) {
37
-
30
+ /**
31
+ * Calculates up/down scores for each comment.
32
+ *
33
+ * Marks if the current user has already voted on the comment.
34
+ * Ensures createdAt timestamp is a string.
35
+ *
36
+ * @param {Object[]} comments
37
+ * @param {Object} req Containing username data
38
+ * @return {Object[]}
39
+ */
40
+ exports.scoreComments = function(comments, req) {
38
41
  return _.map(comments, function(comment) {
39
-
40
42
  comment = _.extend(comment._doc, {
41
43
  score: comment.upVotes.length - comment.downVotes.length,
42
44
  createdAt: String(comment.createdAt)
@@ -51,39 +53,46 @@ exports.formatComments = function(comments, req) {
51
53
  });
52
54
  };
53
55
 
56
+ /**
57
+ * Performs voting on comment.
58
+ *
59
+ * @param {Object} req The request object.
60
+ * @param {Object} res The response object where voting result is written.
61
+ * @param {Comment} comment The comment to vote on.
62
+ */
54
63
  exports.vote = function(req, res, comment) {
55
-
56
64
  var voteDirection;
65
+ var username = req.session.user.username;
57
66
 
58
- if (req.session.user.username == comment.author) {
67
+ if (username == comment.author) {
59
68
 
60
69
  // Ignore votes from the author
61
70
  res.json({success: false, reason: 'You cannot vote on your own content'});
62
71
  return;
63
72
 
64
- } else if (req.body.vote == 'up' && !_.include(comment.upVotes, req.session.user.username)) {
73
+ } else if (req.body.vote == 'up' && !_.include(comment.upVotes, username)) {
65
74
 
66
- var voted = _.include(comment.downVotes, req.session.user.username);
75
+ var voted = _.include(comment.downVotes, username);
67
76
 
68
77
  comment.downVotes = _.reject(comment.downVotes, function(v) {
69
- return v == req.session.user.username;
78
+ return v == username;
70
79
  });
71
80
 
72
81
  if (!voted) {
73
82
  voteDirection = 'up';
74
- comment.upVotes.push(req.session.user.username);
83
+ comment.upVotes.push(username);
75
84
  }
76
- } else if (req.body.vote == 'down' && !_.include(comment.downVotes, req.session.user.username)) {
85
+ } else if (req.body.vote == 'down' && !_.include(comment.downVotes, username)) {
77
86
 
78
- var voted = _.include(comment.upVotes, req.session.user.username);
87
+ var voted = _.include(comment.upVotes, username);
79
88
 
80
89
  comment.upVotes = _.reject(comment.upVotes, function(v) {
81
- return v == req.session.user.username;
90
+ return v == username;
82
91
  });
83
92
 
84
93
  if (!voted) {
85
94
  voteDirection = 'down';
86
- comment.downVotes.push(req.session.user.username);
95
+ comment.downVotes.push(username);
87
96
  }
88
97
  }
89
98
 
@@ -96,9 +105,14 @@ exports.vote = function(req, res, comment) {
96
105
  });
97
106
  };
98
107
 
99
-
108
+ /**
109
+ * Ensures that user is logged in.
110
+ *
111
+ * @param {Object} req
112
+ * @param {Object} res
113
+ * @param {Function} next
114
+ */
100
115
  exports.requireLoggedInUser = function(req, res, next) {
101
-
102
116
  if (!req.session || !req.session.user) {
103
117
  res.json({success: false, reason: 'Forbidden'}, 403);
104
118
  } else {
@@ -106,8 +120,16 @@ exports.requireLoggedInUser = function(req, res, next) {
106
120
  }
107
121
  };
108
122
 
123
+ /**
124
+ * Looks up comment by ID.
125
+ *
126
+ * Stores it into `req.comment`.
127
+ *
128
+ * @param {Object} req
129
+ * @param {Object} res
130
+ * @param {Function} next
131
+ */
109
132
  exports.findComment = function(req, res, next) {
110
-
111
133
  if (req.params.commentId) {
112
134
  Comment.findById(req.params.commentId, function(err, comment) {
113
135
  req.comment = comment;
@@ -116,18 +138,41 @@ exports.findComment = function(req, res, next) {
116
138
  } else {
117
139
  res.json({success: false, reason: 'No such comment'});
118
140
  }
141
+ };
119
142
 
143
+ /**
144
+ * Ensures that user is allowed to modify/delete the comment,
145
+ * that is, he is the owner of the comment or a moderator.
146
+ *
147
+ * @param {Object} req
148
+ * @param {Object} res
149
+ * @param {Function} next
150
+ */
151
+ exports.requireOwner = function(req, res, next) {
152
+ var isModerator = _.include(req.session.user.membergroupids, 7);
153
+ var isAuthor = req.session.user.username == req.comment.author;
154
+
155
+ if (isModerator || isAuthor) {
156
+ next();
157
+ }
158
+ else {
159
+ res.json({ success: false, reason: 'Forbidden' }, 403);
160
+ }
120
161
  };
121
162
 
163
+ /**
164
+ * Sends e-mail updates when comment is posted to a thread that has
165
+ * subscribers.
166
+ *
167
+ * @param {Comment} comment
168
+ */
122
169
  exports.sendEmailUpdates = function(comment) {
123
-
124
170
  var mailTransport = nodemailer.createTransport("SMTP",{
125
171
  host: 'localhost',
126
172
  port: 25
127
173
  });
128
174
 
129
175
  var sendSubscriptionEmail = function(emails) {
130
-
131
176
  var email = emails.shift();
132
177
 
133
178
  if (email) {
@@ -143,7 +188,7 @@ exports.sendEmailUpdates = function(comment) {
143
188
  console.log("Finished sending emails");
144
189
  mailTransport.close();
145
190
  }
146
- }
191
+ };
147
192
 
148
193
  var subscriptionBody = {
149
194
  sdk: comment.sdk,
@@ -154,7 +199,6 @@ exports.sendEmailUpdates = function(comment) {
154
199
  var emails = [];
155
200
 
156
201
  Subscription.find(subscriptionBody, function(err, subscriptions) {
157
-
158
202
  _.each(subscriptions, function(subscription) {
159
203
  var mailOptions = {
160
204
  transport: mailTransport,
@@ -169,7 +213,7 @@ exports.sendEmailUpdates = function(comment) {
169
213
  "Unsubscribe from this thread: http://projects.sencha.com/auth/unsubscribe/" + subscription._id,
170
214
  "Unsubscribe from all threads: http://projects.sencha.com/auth/unsubscribe/" + subscription._id + '?all=true'
171
215
  ].join("\n")
172
- }
216
+ };
173
217
 
174
218
  if (Number(comment.userId) != Number(subscription.userId)) {
175
219
  emails.push(mailOptions);
@@ -182,28 +226,43 @@ exports.sendEmailUpdates = function(comment) {
182
226
  console.log("No emails to send");
183
227
  }
184
228
  });
185
- }
186
-
187
-
188
- exports.getCommentsMeta = function(req, res, next) {
229
+ };
189
230
 
231
+ /**
232
+ * Retrieves comment counts for each target.
233
+ *
234
+ * Stores into `req.commentCounts` field an array like this:
235
+ *
236
+ * [
237
+ * {"_id": "class__Ext__", "value": 3},
238
+ * {"_id": "class__Ext__method-define", "value": 1},
239
+ * {"_id": "class__Ext.Panel__cfg-title", "value": 8}
240
+ * ]
241
+ *
242
+ * @param {Object} req
243
+ * @param {Object} res
244
+ * @param {Function} next
245
+ */
246
+ exports.getCommentCounts = function(req, res, next) {
247
+ // Map each comment into: ("type__Class__member", 1)
190
248
  var map = function() {
191
249
  if (this.target) {
192
250
  emit(this.target.slice(0,3).join('__'), 1);
193
251
  } else {
194
252
  return;
195
253
  }
196
- }
254
+ };
197
255
 
256
+ // Sum comment counts for each target
198
257
  var reduce = function(key, values) {
199
- var i = 0, total = 0;
258
+ var total = 0;
200
259
 
201
- for (; i< values.length; i++) {
260
+ for (var i = 0; i < values.length; i++) {
202
261
  total += values[i];
203
262
  }
204
263
 
205
264
  return total;
206
- }
265
+ };
207
266
 
208
267
  mongoose.connection.db.executeDbCommand({
209
268
  mapreduce: 'comments',
@@ -218,13 +277,29 @@ exports.getCommentsMeta = function(req, res, next) {
218
277
  }, function(err, dbres) {
219
278
  mongoose.connection.db.collection('commentCounts', function(err, collection) {
220
279
  collection.find({}).toArray(function(err, comments) {
221
- req.commentsMeta = comments;
222
- next()
280
+ req.commentCounts = comments;
281
+ next();
223
282
  });
224
283
  });
225
284
  });
226
- }
285
+ };
227
286
 
287
+ /**
288
+ * Retrieves list of commenting targets into which the current user
289
+ * has subscribed for e-mail updates.
290
+ *
291
+ * Stores them into `req.commentSubscriptions` field as array:
292
+ *
293
+ * [
294
+ * ["class", "Ext", ""],
295
+ * ["class", "Ext", "method-define"],
296
+ * ["class", "Ext.Panel", "cfg-title"]
297
+ * ]
298
+ *
299
+ * @param {Object} req
300
+ * @param {Object} res
301
+ * @param {Function} next
302
+ */
228
303
  exports.getCommentSubscriptions = function(req, res, next) {
229
304
  if (req.session.user) {
230
305
  Subscription.find({
@@ -236,10 +311,10 @@ exports.getCommentSubscriptions = function(req, res, next) {
236
311
  return subscription.target;
237
312
  });
238
313
  next();
239
- })
314
+ });
240
315
  } else {
241
316
  next();
242
317
  }
243
- }
318
+ };
244
319
 
245
320