jsduck 3.7.0 → 3.8.0

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.
@@ -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