diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 2444c8cb0d..7ae6cba36c 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -1,5 +1,6 @@ fs = require "fs" PackageVersions = require "./app/coffee/infrastructure/PackageVersions" +require('es6-promise').polyfill() module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-coffee' @@ -18,6 +19,7 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-parallel' grunt.loadNpmTasks 'grunt-exec' + grunt.loadNpmTasks 'grunt-postcss' # grunt.loadNpmTasks 'grunt-contrib-imagemin' # grunt.loadNpmTasks 'grunt-sprity' @@ -136,8 +138,14 @@ module.exports = (grunt) -> files: "public/stylesheets/style.css": "public/stylesheets/style.less" - - + postcss: + options: + map: true, + processors: [ + require('autoprefixer')({browsers: [ 'last 2 versions', 'ie >= 10' ]}) + ] + dist: + src: 'public/stylesheets/style.css' env: run: @@ -366,7 +374,7 @@ module.exports = (grunt) -> grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server'] grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes'] - grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] + grunt.registerTask 'compile:css', 'Compile the less files to css', ['less', 'postcss:dist'] grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee index 65013eae46..9c3a9f4deb 100644 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee +++ b/services/web/app/coffee/Features/Announcements/AnnouncementsController.coffee @@ -9,11 +9,11 @@ module.exports = if !settings?.apis?.analytics?.url? or !settings.apis.blog.url? return res.json [] - user_id = AuthenticationController.getLoggedInUserId(req) - logger.log {user_id}, "getting unread announcements" - AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)-> + user = AuthenticationController.getSessionUser(req) + logger.log {user_id:user?._id}, "getting unread announcements" + AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)-> if err? - logger.err {err, user_id}, "unable to get unread announcements" + logger.err {err:err, user_id:user._id}, "unable to get unread announcements" next(err) else res.json announcements diff --git a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee index ce41e3b96c..9934a8bf69 100644 --- a/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee +++ b/services/web/app/coffee/Features/Announcements/AnnouncementsHandler.coffee @@ -1,24 +1,46 @@ AnalyticsManager = require("../Analytics/AnalyticsManager") BlogHandler = require("../Blog/BlogHandler") -async = require("async") -_ = require("lodash") logger = require("logger-sharelatex") settings = require("settings-sharelatex") +async = require("async") +_ = require("lodash") -module.exports = +module.exports = AnnouncementsHandler = + + _domainSpecificAnnouncements : (email)-> + domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)-> + matches = _.filter domainAnnouncment.domains, (domain)-> + return email.indexOf(domain) != -1 + return matches.length > 0 and domainAnnouncment.id? + return domainSpecific or [] + + + getUnreadAnnouncements : (user, callback = (err, announcements)->)-> + if !user? and !user._id? + return callback("user not supplied") - getUnreadAnnouncements : (user_id, callback = (err, announcements)->)-> async.parallel { lastEvent: (cb)-> - AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb + AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb announcements: (cb)-> BlogHandler.getLatestAnnouncements cb }, (err, results)-> if err? - logger.err err:err, user_id:user_id, "error getting unread announcements" + logger.err err:err, user_id:user._id, "error getting unread announcements" return callback(err) - announcements = _.sortBy(results.announcements, "date").reverse() + domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email) + + domainSpecific = _.map domainSpecific, (domainAnnouncment)-> + try + domainAnnouncment.date = new Date(domainAnnouncment.date) + return domainAnnouncment + catch e + return callback(e) + + announcements = results.announcements + announcements = _.union announcements, domainSpecific + announcements = _.sortBy(announcements, "date").reverse() lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId @@ -35,6 +57,6 @@ module.exports = announcement.read = read return announcement - logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements" + logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements" callback null, announcements diff --git a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee index aa4b75ce11..3cae19b7f3 100644 --- a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee +++ b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee @@ -58,4 +58,25 @@ module.exports = ChatApiHandler = ChatApiHandler._apiRequest { url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen" method: "POST" - }, callback \ No newline at end of file + }, callback + + deleteThread: (project_id, thread_id, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}" + method: "DELETE" + }, callback + + editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit" + method: "POST" + json: + content: content + }, callback + + deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}" + method: "DELETE" + }, callback + \ No newline at end of file diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee index ee9b8b9f84..bda006eb8f 100644 --- a/services/web/app/coffee/Features/Comments/CommentsController.coffee +++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee @@ -4,6 +4,7 @@ logger = require("logger-sharelatex") AuthenticationController = require('../Authentication/AuthenticationController') UserInfoManager = require('../User/UserInfoManager') UserInfoController = require('../User/UserInfoController') +DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" async = require "async" module.exports = CommentsController = @@ -50,6 +51,33 @@ module.exports = CommentsController = return next(err) if err? EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)-> res.send 204 + + deleteThread: (req, res, next) -> + {project_id, doc_id, thread_id} = req.params + logger.log {project_id, doc_id, thread_id}, "deleting comment thread" + DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) -> + return next(err) if err? + ChatApiHandler.deleteThread project_id, thread_id, (err, threads) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)-> + res.send 204 + + editMessage: (req, res, next) -> + {project_id, thread_id, message_id} = req.params + {content} = req.body + logger.log {project_id, thread_id, message_id}, "editing message thread" + ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)-> + res.send 204 + + deleteMessage: (req, res, next) -> + {project_id, thread_id, message_id} = req.params + logger.log {project_id, thread_id, message_id}, "deleting message" + ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_id, (err)-> + res.send 204 _injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) -> userCache = {} diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index bb4922704f..5c15735410 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -153,6 +153,22 @@ module.exports = DocumentUpdaterHandler = logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}" callback new Error("doc updater returned a non-success status code: #{res.statusCode}") + deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) -> + timer = new metrics.Timer("delete-thread") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}" + logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater" + request.del url, (error, res, body)-> + timer.done() + if error? + logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater" + return callback(error) + if res.statusCode >= 200 and res.statusCode < 300 + logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater" + return callback(null) + else + logger.error {project_id, doc_id, thread_id}, "doc updater returned a non-success status code: #{res.statusCode}" + callback new Error("doc updater returned a non-success status code: #{res.statusCode}") + PENDINGUPDATESKEY = "PendingUpdates" DOCLINESKEY = "doclines" DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates" diff --git a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee index a8453f9b81..20943628ed 100644 --- a/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee +++ b/services/web/app/coffee/Features/Security/LoginRateLimiter.coffee @@ -1,23 +1,21 @@ -Settings = require('settings-sharelatex') -redis = require("redis-sharelatex") -rclient = redis.createClient(Settings.redis.web) +RateLimiter = require('../../infrastructure/RateLimiter') -buildKey = (k)-> - return "LoginRateLimit:#{k}" ONE_MIN = 60 ATTEMPT_LIMIT = 10 + module.exports = - processLoginRequest: (email, callback)-> - multi = rclient.multi() - multi.incr(buildKey(email)) - multi.get(buildKey(email)) - multi.expire(buildKey(email), ONE_MIN * 2) - multi.exec (err, results)-> - loginCount = results[1] - allow = loginCount <= ATTEMPT_LIMIT - callback err, allow + + processLoginRequest: (email, callback) -> + opts = + endpointName: 'login' + throttle: ATTEMPT_LIMIT + timeInterval: ONE_MIN * 2 + subjectName: email + RateLimiter.addCount opts, (err, shouldAllow) -> + callback(err, shouldAllow) recordSuccessfulLogin: (email, callback = ->)-> - rclient.del buildKey(email), callback \ No newline at end of file + RateLimiter.clearRateLimit 'login', email, callback + diff --git a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee index 005f2d23d3..b813878335 100755 --- a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee +++ b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee @@ -6,8 +6,6 @@ Project = require('../../models/Project').Project DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') Settings = require('settings-sharelatex') util = require('util') -redis = require("redis-sharelatex") -rclient = redis.createClient(Settings.redis.web) RecurlyWrapper = require('../Subscription/RecurlyWrapper') SubscriptionHandler = require('../Subscription/SubscriptionHandler') projectEntityHandler = require('../Project/ProjectEntityHandler') diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 78016e8a09..2cd3b17e7f 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -1,5 +1,4 @@ Settings = require('settings-sharelatex') -redis = require('redis-sharelatex') logger = require("logger-sharelatex") Async = require('async') _ = require('underscore') diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 867583468b..8124d13d93 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)-> return AuthenticationController.isUserLoggedIn(req) res.locals.getSessionUser = -> return AuthenticationController.getSessionUser(req) + next() webRouter.use (req, res, next) -> diff --git a/services/web/app/coffee/infrastructure/RateLimiter.coffee b/services/web/app/coffee/infrastructure/RateLimiter.coffee index 7c84fc9db7..c749fa7e83 100644 --- a/services/web/app/coffee/infrastructure/RateLimiter.coffee +++ b/services/web/app/coffee/infrastructure/RateLimiter.coffee @@ -1,15 +1,27 @@ settings = require("settings-sharelatex") -redis = require("redis-sharelatex") -rclient = redis.createClient(settings.redis.web) -redback = require("redback").use(rclient) +RedisWrapper = require('./RedisWrapper') +rclient = RedisWrapper.client('ratelimiter') +RollingRateLimiter = require('rolling-rate-limiter') -module.exports = - addCount: (opts, callback = (opts, shouldProcess)->)-> - ratelimit = redback.createRateLimit(opts.endpointName) - ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)-> - shouldProcess = callCount < opts.throttle - callback(err, shouldProcess) - +module.exports = RateLimiter = + + addCount: (opts, callback = (err, shouldProcess)->)-> + namespace = "RateLimit:#{opts.endpointName}:" + k = "{#{opts.subjectName}}" + limiter = RollingRateLimiter({ + redis: rclient, + namespace: namespace, + interval: opts.timeInterval * 1000, + maxInInterval: opts.throttle + }) + limiter k, (err, timeLeft, actionsLeft) -> + if err? + return callback(err) + allowed = timeLeft == 0 + callback(null, allowed) + clearRateLimit: (endpointName, subject, callback) -> - rclient.del "#{endpointName}:#{subject}", callback \ No newline at end of file + # same as the key which will be built by RollingRateLimiter (namespace+k) + keyName = "RateLimit:#{endpointName}:{#{subject}}" + rclient.del keyName, callback diff --git a/services/web/app/coffee/infrastructure/RedisWrapper.coffee b/services/web/app/coffee/infrastructure/RedisWrapper.coffee new file mode 100644 index 0000000000..5d8b5836b5 --- /dev/null +++ b/services/web/app/coffee/infrastructure/RedisWrapper.coffee @@ -0,0 +1,28 @@ +Settings = require 'settings-sharelatex' +redis = require 'redis-sharelatex' +ioredis = require 'ioredis' +logger = require 'logger-sharelatex' + + +# A per-feature interface to Redis, +# looks up the feature in `settings.redis` +# and returns an appropriate client. +# Necessary because we don't want to migrate web over +# to redis-cluster all at once. + +# TODO: consider merging into `redis-sharelatex` + + +module.exports = Redis = + + # feature = 'websessions' | 'ratelimiter' | ... + client: (feature) -> + redisFeatureSettings = Settings.redis[feature] or Settings.redis.web + if redisFeatureSettings?.cluster? + logger.log {feature}, "creating redis-cluster client" + rclient = new ioredis.Cluster(redisFeatureSettings.cluster) + rclient.__is_redis_cluster = true + else + logger.log {feature}, "creating redis client" + rclient = redis.createClient(redisFeatureSettings) + return rclient diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 43683bdd4e..01d431fa49 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -7,7 +7,6 @@ crawlerLogger = require('./CrawlerLogger') expressLocals = require('./ExpressLocals') Router = require('../router') metrics.inc("startup") -redis = require("redis-sharelatex") UserSessionsRedis = require('../Features/User/UserSessionsRedis') sessionsRedisClient = UserSessionsRedis.client() diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index a9105a1d46..62d5ec0865 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -238,6 +238,9 @@ module.exports = class Router webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread + webRouter.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread + webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage + webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index e02b9ec3d0..bcb778fda4 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -59,15 +59,6 @@ div.full-size( renderer-data="reviewPanel.rendererData" ) - a.rp-track-changes-indicator( - href - ng-if="editor.wantTrackChanges" - ng-click="toggleReviewPanel();" - ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" - ) Track changes is - strong on - - include ./review-panel .ui-layout-east diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 0419884502..590c1a6776 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -1,13 +1,22 @@ #review-panel + a.rp-track-changes-indicator( + href + ng-if="editor.wantTrackChanges" + ng-click="toggleReviewPanel();" + ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" + ) Track changes is + strong on + .review-panel-toolbar resolved-comments-dropdown( + class="rp-flex-block" entries="reviewPanel.resolvedComments" threads="reviewPanel.commentThreads" resolved-ids="reviewPanel.resolvedThreadIds" docs="docs" on-open="refreshResolvedCommentsDropdown();" on-unresolve="unresolveComment(threadId);" - on-delete="deleteComment(entryId, threadId);" + on-delete="deleteThread(entryId, docId, threadId);" is-loading="reviewPanel.dropdown.loading" permissions="permissions" ) @@ -32,7 +41,6 @@ .rp-entry-list-inner .rp-entry-wrapper( ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]" - ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)" ) div(ng-if="entry.type === 'insert' || entry.type === 'delete'") change-entry( @@ -51,6 +59,8 @@ on-resolve="resolveComment(entry, entry_id)" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" + on-save-edit="saveEdit(entry.thread_id, comment)" + on-delete="deleteComment(entry.thread_id, comment)" permissions="permissions" ng-if="!reviewPanel.loadingThreads" ) @@ -94,6 +104,8 @@ entry="entry" threads="reviewPanel.commentThreads" on-reply="submitReply(entry, entry_id);" + on-save-edit="saveEdit(entry.thread_id, comment)" + on-delete="deleteComment(entry.thread_id, comment)" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc.doc.id, entry)" permissions="permissions" @@ -175,21 +187,48 @@ script(type='text/ng-template', id='commentEntryTemplate') .rp-entry.rp-entry-comment( ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }" ) + + .rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)") + | No comments div .rp-comment( ng-repeat="comment in threads[entry.thread_id].messages track by comment.id" ) - p.rp-comment-content - span.rp-entry-user( - style="color: hsl({{ comment.user.hue }}, 70%, 40%);" - ) {{ comment.user.name }}:  - | {{ comment.content }} - .rp-entry-metadata - | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} - .rp-loading(ng-if="!threads[entry.thread_id] || threads[entry.thread_id].submitting") + p.rp-comment-content + span(ng-if="!comment.editing") + span.rp-entry-user( + style="color: hsl({{ comment.user.hue }}, 70%, 40%);", + ) {{ comment.user.name }}:  + | {{ comment.content }} + textarea.rp-comment-input( + expandable-text-area + ng-if="comment.editing" + ng-model="comment.content" + ng-keypress="saveEditOnEnter($event, comment);" + ng-blur="saveEdit(comment)" + autofocus + stop-propagation="click" + ) + .rp-entry-metadata(ng-if="!comment.editing") + span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting") + |  •  + a(href, ng-click="startEditing(comment)") Edit + span(ng-if="threads[entry.thread_id].messages.length > 1") + |  •  + a(href, ng-click="confirmDelete(comment)") Delete + span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting") + | Are you sure? + | •  + a(href, ng-click="doDelete(comment)") Delete + |  •  + a(href, ng-click="cancelDelete(comment)") Cancel + + .rp-loading(ng-if="threads[entry.thread_id].submitting") i.fa.fa-spinner.fa-spin .rp-comment-reply(ng-if="permissions.comment") textarea.rp-comment-input( + expandable-text-area ng-model="entry.replyContent" ng-keypress="handleCommentReplyKeyPress($event);" stop-propagation="click" @@ -249,11 +288,11 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') ng-click="onUnresolve({ 'threadId': thread.threadId });" ) |  Re-open - //- a.rp-entry-button( - //- href - //- ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });" - //- ) - //- |  Delete + a.rp-entry-button( + href + ng-click="onDelete({ 'entryId': thread.entryId, 'docId': thread.docId, 'threadId': thread.threadId });" + ) + |  Delete script(type='text/ng-template', id='addCommentEntryTemplate') @@ -280,6 +319,7 @@ script(type='text/ng-template', id='addCommentEntryTemplate') div(ng-if="state.isAdding") .rp-new-comment textarea.rp-comment-input( + expandable-text-area ng-model="state.content" ng-keypress="handleCommentKeyPress($event);" placeholder="Add your comment here" @@ -324,7 +364,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true" thread="thread" on-unresolve="handleUnresolve(threadId);" - on-delete="handleDelete(entryId, threadId);" + on-delete="handleDelete(entryId, docId, threadId);" permissions="permissions" ) .rp-loading(ng-if="!resolvedComments.length") diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index d9fbf0e6b1..f707cd9411 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -73,4 +73,4 @@ block content .col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8 include ./list/empty-project-list - include ./list/modals \ No newline at end of file + include ./list/modals diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 44e4a75867..8e503801f9 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -48,6 +48,16 @@ module.exports = settings = # {host: 'localhost', port: 7005} # ] + # ratelimiter: + # cluster: [ + # {host: 'localhost', port: 7000} + # {host: 'localhost', port: 7001} + # {host: 'localhost', port: 7002} + # {host: 'localhost', port: 7003} + # {host: 'localhost', port: 7004} + # {host: 'localhost', port: 7005} + # ] + api: host: "localhost" port: "6379" diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index 6fbb560b79..ebc9f188b7 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -8,8 +8,7 @@ "dependencies": { "buffer-crc32": { "version": "0.2.13", - "from": "buffer-crc32@~0.2.1", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" + "from": "buffer-crc32@~0.2.1" }, "readable-stream": { "version": "1.0.34", @@ -21,7 +20,8 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1" + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", @@ -101,7 +101,7 @@ }, "glob": { "version": "3.2.11", - "from": "glob@~3.2.6", + "from": "glob@~3.2.9", "dependencies": { "inherits": { "version": "2.0.3", @@ -125,7 +125,7 @@ }, "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.12", + "from": "minimatch@~0.2.9", "dependencies": { "lru-cache": { "version": "2.7.3", @@ -171,7 +171,6 @@ "readable-stream": { "version": "2.2.2", "from": "readable-stream@^2.0.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -219,8 +218,7 @@ }, "nan": { "version": "1.8.4", - "from": "nan@1.8.4", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" + "from": "nan@1.8.4" } } }, @@ -234,11 +232,11 @@ }, "content-type": { "version": "1.0.2", - "from": "content-type@~1.0.1" + "from": "content-type@~1.0.2" }, "debug": { "version": "2.6.0", - "from": "debug@^2.2.0", + "from": "debug@*", "dependencies": { "ms": { "version": "0.7.2", @@ -248,12 +246,11 @@ }, "depd": { "version": "1.1.0", - "from": "depd@^1.1.0" + "from": "depd@~1.1.0" }, "http-errors": { "version": "1.5.1", "from": "http-errors@~1.5.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", "dependencies": { "inherits": { "version": "2.0.3", @@ -266,14 +263,13 @@ }, "statuses": { "version": "1.3.1", - "from": "statuses@>= 1.3.1 < 2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" + "from": "statuses@>= 1.3.1 < 2" } } }, "iconv-lite": { "version": "0.4.15", - "from": "iconv-lite@~0.4.13", + "from": "iconv-lite@0.4.15", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz" }, "on-finished": { @@ -303,8 +299,7 @@ }, "type-is": { "version": "1.6.14", - "from": "type-is@~1.6.10", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.14.tgz", + "from": "type-is@~1.6.14", "dependencies": { "media-typer": { "version": "0.3.0", @@ -351,13 +346,14 @@ "from": "double-ended-queue@^2.1.0-0" }, "redis-commands": { - "version": "1.3.0", - "from": "redis-commands@^1.2.0" + "version": "1.3.1", + "from": "redis-commands@^1.2.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz" }, "redis-parser": { - "version": "2.3.0", + "version": "2.4.0", "from": "redis-parser@^2.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.3.0.tgz" + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.4.0.tgz" } } } @@ -366,24 +362,20 @@ "contentful": { "version": "3.8.0", "from": "contentful@^3.3.14", - "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.8.0.tgz", "dependencies": { "babel-runtime": { "version": "6.3.19", "from": "babel-runtime@~6.3.19", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.3.19.tgz", "dependencies": { "core-js": { "version": "1.2.7", - "from": "core-js@^1.2.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + "from": "core-js@^1.2.0" } } }, "contentful-sdk-core": { "version": "2.5.0", "from": "contentful-sdk-core@~2.5.0", - "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.5.0.tgz", "dependencies": { "follow-redirects": { "version": "0.0.7", @@ -407,8 +399,7 @@ }, "qs": { "version": "6.3.0", - "from": "qs@^6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz" + "from": "qs@^6.1.0" } } }, @@ -418,8 +409,7 @@ }, "lodash": { "version": "4.2.1", - "from": "lodash@~4.2.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.2.1.tgz" + "from": "lodash@~4.2.0" } } }, @@ -434,7 +424,8 @@ "dependencies": { "cookie": { "version": "0.1.3", - "from": "cookie@0.1.3" + "from": "cookie@0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" }, "cookie-signature": { "version": "1.0.6", @@ -457,24 +448,25 @@ "csrf": { "version": "3.0.4", "from": "csrf@~3.0.3", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.4.tgz", "dependencies": { "base64-url": { "version": "1.3.3", - "from": "base64-url@1.3.3", - "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.3.3.tgz" + "from": "base64-url@1.3.3" }, "rndm": { "version": "1.2.0", - "from": "rndm@1.2.0" + "from": "rndm@1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz" }, "tsscmp": { "version": "1.0.5", - "from": "tsscmp@1.0.5" + "from": "tsscmp@1.0.5", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz" }, "uid-safe": { "version": "2.1.3", "from": "uid-safe@~2.1.3", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.3.tgz", "dependencies": { "random-bytes": { "version": "1.0.0", @@ -487,11 +479,10 @@ "http-errors": { "version": "1.5.1", "from": "http-errors@~1.5.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", "dependencies": { "inherits": { "version": "2.0.3", - "from": "inherits@~2.0.1" + "from": "inherits@2.0.3" }, "setprototypeof": { "version": "1.0.2", @@ -500,8 +491,7 @@ }, "statuses": { "version": "1.3.1", - "from": "statuses@>= 1.3.1 < 2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" + "from": "statuses@>= 1.3.1 < 2" } } } @@ -550,7 +540,8 @@ }, "cookie": { "version": "0.1.3", - "from": "cookie@0.1.3" + "from": "cookie@0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" }, "cookie-signature": { "version": "1.0.6", @@ -572,7 +563,8 @@ }, "escape-html": { "version": "1.0.2", - "from": "escape-html@1.0.2" + "from": "escape-html@1.0.2", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz" }, "etag": { "version": "1.7.0", @@ -594,7 +586,8 @@ }, "merge-descriptors": { "version": "1.0.0", - "from": "merge-descriptors@1.0.0" + "from": "merge-descriptors@1.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz" }, "methods": { "version": "1.1.2", @@ -616,7 +609,8 @@ }, "path-to-regexp": { "version": "0.1.6", - "from": "path-to-regexp@0.1.6" + "from": "path-to-regexp@0.1.6", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.6.tgz" }, "proxy-addr": { "version": "1.0.10", @@ -634,8 +628,7 @@ }, "qs": { "version": "2.4.2", - "from": "qs@2.4.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz" + "from": "qs@2.4.2" }, "range-parser": { "version": "1.0.3", @@ -661,7 +654,7 @@ }, "mime": { "version": "1.3.4", - "from": "mime@1.3.4" + "from": "mime@^1.2.9" }, "ms": { "version": "0.7.1", @@ -705,7 +698,7 @@ }, "mime": { "version": "1.3.4", - "from": "mime@1.3.4" + "from": "mime@^1.2.9" }, "ms": { "version": "0.7.1", @@ -721,8 +714,7 @@ }, "type-is": { "version": "1.6.14", - "from": "type-is@~1.6.10", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.14.tgz", + "from": "type-is@~1.6.14", "dependencies": { "media-typer": { "version": "0.3.0", @@ -753,7 +745,6 @@ "express-session": { "version": "1.15.0", "from": "express-session@^1.14.2", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.0.tgz", "dependencies": { "cookie": { "version": "0.3.1", @@ -770,7 +761,7 @@ }, "debug": { "version": "2.6.0", - "from": "debug@^2.2.0", + "from": "debug@*", "dependencies": { "ms": { "version": "0.7.2", @@ -784,8 +775,7 @@ }, "on-headers": { "version": "1.0.1", - "from": "on-headers@~1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz" + "from": "on-headers@~1.0.1" }, "parseurl": { "version": "1.3.1", @@ -794,11 +784,11 @@ "uid-safe": { "version": "2.1.3", "from": "uid-safe@~2.1.3", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.3.tgz", "dependencies": { "base64-url": { "version": "1.3.3", - "from": "base64-url@1.3.3", - "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.3.3.tgz" + "from": "base64-url@1.3.3" }, "random-bytes": { "version": "1.0.0", @@ -830,7 +820,8 @@ }, "dateformat": { "version": "1.0.2-1.2.3", - "from": "dateformat@1.0.2-1.2.3" + "from": "dateformat@1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" }, "eventemitter2": { "version": "0.4.14", @@ -996,7 +987,6 @@ "http-proxy": { "version": "1.16.2", "from": "http-proxy@^1.8.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.16.2.tgz", "dependencies": { "eventemitter3": { "version": "1.2.0", @@ -1014,8 +1004,7 @@ "dependencies": { "bluebird": { "version": "3.4.7", - "from": "bluebird@^3.3.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz" + "from": "bluebird@^3.3.4" }, "cluster-key-slot": { "version": "1.0.8", @@ -1023,7 +1012,7 @@ }, "debug": { "version": "2.6.0", - "from": "debug@^2.2.0", + "from": "debug@*", "dependencies": { "ms": { "version": "0.7.2", @@ -1037,12 +1026,12 @@ }, "flexbuffer": { "version": "0.0.6", - "from": "flexbuffer@0.0.6", - "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz" + "from": "flexbuffer@0.0.6" }, "redis-commands": { - "version": "1.3.0", - "from": "redis-commands@^1.2.0" + "version": "1.3.1", + "from": "redis-commands@^1.2.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz" }, "redis-parser": { "version": "1.3.0", @@ -1061,8 +1050,7 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "from": "mkdirp@~0.3.5" }, "transformers": { "version": "2.1.0", @@ -1102,8 +1090,7 @@ "dependencies": { "amdefine": { "version": "1.0.1", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz" + "from": "amdefine@>=0.0.4" } } }, @@ -1175,8 +1162,7 @@ "dependencies": { "amdefine": { "version": "1.0.1", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz" + "from": "amdefine@>=0.0.4" } } }, @@ -1198,7 +1184,8 @@ }, "window-size": { "version": "0.1.0", - "from": "window-size@0.1.0" + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" }, "wordwrap": { "version": "0.0.2", @@ -1216,7 +1203,7 @@ "dependencies": { "uglify-js": { "version": "2.4.24", - "from": "uglify-js@~2.4.12", + "from": "uglify-js@~2.4.0", "dependencies": { "async": { "version": "0.2.10", @@ -1228,8 +1215,7 @@ "dependencies": { "amdefine": { "version": "1.0.1", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz" + "from": "amdefine@>=0.0.4" } } }, @@ -1251,7 +1237,8 @@ }, "window-size": { "version": "0.1.0", - "from": "window-size@0.1.0" + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" }, "wordwrap": { "version": "0.0.2", @@ -1268,7 +1255,6 @@ "ldapjs": { "version": "1.0.1", "from": "ldapjs@^1.0.0", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.1.tgz", "dependencies": { "asn1": { "version": "0.2.3", @@ -1281,17 +1267,14 @@ "bunyan": { "version": "1.8.5", "from": "bunyan@^1.8.3", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.5.tgz", "dependencies": { "dtrace-provider": { "version": "0.8.0", "from": "dtrace-provider@~0.8", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.0.tgz", "dependencies": { "nan": { "version": "2.5.1", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz" + "from": "nan@^2.3.3" } } }, @@ -1305,8 +1288,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "from": "minimist@0.0.8" } } }, @@ -1325,7 +1307,6 @@ "inflight": { "version": "1.0.6", "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1359,8 +1340,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "from": "path-is-absolute@^1.0.0" } } } @@ -1374,20 +1354,17 @@ }, "moment": { "version": "2.17.1", - "from": "moment@^2.10.6", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.1.tgz" + "from": "moment@^2.10.6" } } }, "dashdash": { "version": "1.14.1", - "from": "dashdash@^1.14.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + "from": "dashdash@^1.14.0" }, "backoff": { "version": "2.5.0", "from": "backoff@^2.5.0", - "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", "dependencies": { "precond": { "version": "0.2.3", @@ -1408,7 +1385,6 @@ "once": { "version": "1.4.0", "from": "once@^1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1419,7 +1395,6 @@ "vasync": { "version": "1.6.4", "from": "vasync@^1.6.4", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz", "dependencies": { "verror": { "version": "1.6.0", @@ -1436,28 +1411,24 @@ "verror": { "version": "1.9.0", "from": "verror@^1.8.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.9.0.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@1.0.2" + "from": "core-util-is@~1.0.0" }, "extsprintf": { "version": "1.3.0", - "from": "extsprintf@^1.2.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + "from": "extsprintf@^1.2.0" } } }, "dtrace-provider": { "version": "0.7.1", "from": "dtrace-provider@^0.7.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.7.1.tgz", "dependencies": { "nan": { "version": "2.5.1", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz" + "from": "nan@^2.0.8" } } } @@ -1482,8 +1453,7 @@ "dependencies": { "nan": { "version": "2.5.1", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz" + "from": "nan@^2.0.8" } } }, @@ -1497,8 +1467,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "from": "minimist@0.0.8" } } }, @@ -1517,7 +1486,6 @@ "inflight": { "version": "1.0.6", "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1552,7 +1520,6 @@ "once": { "version": "1.4.0", "from": "once@^1.3.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -1562,8 +1529,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "from": "path-is-absolute@^1.0.0" } } } @@ -1579,7 +1545,8 @@ }, "coffee-script": { "version": "1.4.0", - "from": "coffee-script@1.4.0" + "from": "coffee-script@1.4.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz" }, "raven": { "version": "0.8.1", @@ -1626,11 +1593,11 @@ "method-override": { "version": "2.3.7", "from": "method-override@^2.3.3", - "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.7.tgz", "dependencies": { "debug": { "version": "2.3.3", "from": "debug@2.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz", "dependencies": { "ms": { "version": "0.7.2", @@ -1640,11 +1607,11 @@ }, "methods": { "version": "1.1.2", - "from": "methods@~1.1.2" + "from": "methods@~1.1.1" }, "parseurl": { "version": "1.3.1", - "from": "parseurl@~1.3.1" + "from": "parseurl@~1.3.0" }, "vary": { "version": "1.1.0", @@ -1659,7 +1626,8 @@ "dependencies": { "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0" + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" } } }, @@ -1691,8 +1659,7 @@ "dependencies": { "commander": { "version": "2.0.0", - "from": "commander@2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" + "from": "commander@2.0.0" }, "growl": { "version": "1.7.0", @@ -1705,13 +1672,11 @@ "dependencies": { "commander": { "version": "0.6.1", - "from": "commander@0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + "from": "commander@0.6.1" }, "mkdirp": { "version": "0.3.0", - "from": "mkdirp@0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + "from": "mkdirp@0.3.0" } } }, @@ -1721,7 +1686,7 @@ }, "debug": { "version": "2.6.0", - "from": "debug@^2.2.0", + "from": "debug@*", "dependencies": { "ms": { "version": "0.7.2", @@ -1731,13 +1696,11 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "from": "mkdirp@~0.3.5" }, "glob": { "version": "3.2.3", "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "dependencies": { "minimatch": { "version": "0.2.14", @@ -1768,6 +1731,7 @@ "mongojs": { "version": "0.18.2", "from": "mongojs@0.18.2", + "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz", "dependencies": { "thunky": { "version": "0.1.0", @@ -1775,7 +1739,7 @@ }, "readable-stream": { "version": "1.1.14", - "from": "readable-stream@1.1.x", + "from": "readable-stream@~1.1.9", "dependencies": { "core-util-is": { "version": "1.0.2", @@ -1783,7 +1747,8 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1" + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", @@ -1806,26 +1771,24 @@ "dependencies": { "nan": { "version": "1.8.4", - "from": "nan@~1.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" + "from": "nan@~1.8" } } }, "kerberos": { "version": "0.0.9", "from": "kerberos@0.0.9", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz", "dependencies": { "nan": { "version": "1.6.2", - "from": "nan@1.6.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz" + "from": "nan@1.6.2" } } }, "readable-stream": { "version": "2.2.2", "from": "readable-stream@latest", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -1902,7 +1865,6 @@ "mongodb": { "version": "2.0.34", "from": "mongodb@2.0.34", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz", "dependencies": { "mongodb-core": { "version": "1.2.0", @@ -1915,7 +1877,6 @@ "kerberos": { "version": "0.0.22", "from": "kerberos@~0.0", - "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.22.tgz", "dependencies": { "nan": { "version": "2.4.0", @@ -1928,7 +1889,6 @@ "readable-stream": { "version": "1.0.31", "from": "readable-stream@1.0.31", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", @@ -1936,7 +1896,8 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1" + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", @@ -1961,14 +1922,17 @@ "mquery": { "version": "1.6.1", "from": "mquery@1.6.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz", "dependencies": { "bluebird": { "version": "2.9.26", - "from": "bluebird@2.9.26" + "from": "bluebird@2.9.26", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz" }, "debug": { "version": "2.2.0", - "from": "debug@2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", @@ -2024,7 +1988,8 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1" + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", @@ -2071,7 +2036,6 @@ "nodemailer": { "version": "2.1.0", "from": "nodemailer@2.1.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz", "dependencies": { "libmime": { "version": "2.0.0", @@ -2079,8 +2043,7 @@ "dependencies": { "iconv-lite": { "version": "0.4.13", - "from": "iconv-lite@0.4.13", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + "from": "iconv-lite@0.4.13" }, "libbase64": { "version": "0.1.0", @@ -2205,12 +2168,11 @@ "nodemailer-ses-transport": { "version": "1.5.0", "from": "nodemailer-ses-transport@^1.3.0", - "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.5.0.tgz", "dependencies": { "aws-sdk": { - "version": "2.7.27", + "version": "2.9.0", "from": "aws-sdk@^2.6.12", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.7.27.tgz", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.9.0.tgz", "dependencies": { "buffer": { "version": "4.9.1", @@ -2218,13 +2180,11 @@ "dependencies": { "base64-js": { "version": "1.2.0", - "from": "base64-js@^1.0.2", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz" + "from": "base64-js@^1.0.2" }, "ieee754": { "version": "1.1.8", - "from": "ieee754@^1.1.4", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz" + "from": "ieee754@^1.1.4" }, "isarray": { "version": "1.0.0", @@ -2246,7 +2206,8 @@ }, "sax": { "version": "1.1.5", - "from": "sax@1.1.5" + "from": "sax@1.1.5", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz" }, "url": { "version": "0.10.3", @@ -2265,7 +2226,8 @@ }, "xml2js": { "version": "0.4.15", - "from": "xml2js@0.4.15" + "from": "xml2js@0.4.15", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz" }, "xmlbuilder": { "version": "2.6.2", @@ -2301,7 +2263,7 @@ "dependencies": { "passport-strategy": { "version": "1.0.0", - "from": "passport-strategy@1.x.x" + "from": "passport-strategy@*" }, "pause": { "version": "0.0.1", @@ -2315,7 +2277,7 @@ "dependencies": { "passport-strategy": { "version": "1.0.0", - "from": "passport-strategy@1.x.x" + "from": "passport-strategy@*" }, "ldapauth-fork": { "version": "2.5.5", @@ -2323,8 +2285,7 @@ "dependencies": { "bcryptjs": { "version": "2.3.0", - "from": "bcryptjs@2.3.0", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.3.0.tgz" + "from": "bcryptjs@2.3.0" }, "lru-cache": { "version": "3.2.0", @@ -2379,7 +2340,6 @@ "xml-crypto": { "version": "0.8.5", "from": "xml-crypto@0.8.x", - "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz", "dependencies": { "xmldom": { "version": "0.1.19", @@ -2387,20 +2347,17 @@ }, "xpath.js": { "version": "1.0.7", - "from": "xpath.js@>=0.0.3", - "resolved": "https://registry.npmjs.org/xpath.js/-/xpath.js-1.0.7.tgz" + "from": "xpath.js@>=0.0.3" } } }, "xmldom": { "version": "0.1.27", - "from": "xmldom@0.1.x", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz" + "from": "xmldom@0.1.x" }, "xmlbuilder": { "version": "2.5.2", "from": "xmlbuilder@2.5.x", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.5.2.tgz", "dependencies": { "lodash": { "version": "3.2.0", @@ -2411,7 +2368,6 @@ "xml-encryption": { "version": "0.7.4", "from": "xml-encryption@~0.7", - "resolved": "https://registry.npmjs.org/xml-encryption/-/xml-encryption-0.7.4.tgz", "dependencies": { "ejs": { "version": "0.8.8", @@ -2433,10 +2389,6 @@ } } }, - "redback": { - "version": "0.4.0", - "from": "redback@0.4.0" - }, "redis": { "version": "0.10.1", "from": "redis@0.10.1" @@ -2444,11 +2396,11 @@ "redis-sharelatex": { "version": "0.0.9", "from": "redis-sharelatex@0.0.9", + "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz", "dependencies": { "chai": { "version": "1.9.1", "from": "chai@1.9.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz", "dependencies": { "assertion-error": { "version": "1.0.0", @@ -2460,8 +2412,7 @@ "dependencies": { "type-detect": { "version": "0.1.1", - "from": "type-detect@0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" + "from": "type-detect@0.1.1" } } } @@ -2473,8 +2424,7 @@ "dependencies": { "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "from": "mkdirp@~0.3.5" } } }, @@ -2488,8 +2438,7 @@ "dependencies": { "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "from": "mkdirp@~0.3.5" } } }, @@ -2559,20 +2508,17 @@ "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "from": "minimist@0.0.8" } } }, "jsonfile": { "version": "2.4.0", "from": "jsonfile@^2.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "dependencies": { "graceful-fs": { "version": "4.1.11", - "from": "graceful-fs@^4.1.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + "from": "graceful-fs@^4.1.6" } } }, @@ -2583,7 +2529,6 @@ "glob": { "version": "7.1.1", "from": "glob@^7.0.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", "dependencies": { "fs.realpath": { "version": "1.0.0", @@ -2592,7 +2537,6 @@ "inflight": { "version": "1.0.6", "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -2627,7 +2571,6 @@ "once": { "version": "1.4.0", "from": "once@^1.3.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -2637,8 +2580,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "from": "path-is-absolute@^1.0.0" } } } @@ -2655,8 +2597,7 @@ "dependencies": { "commander": { "version": "2.0.0", - "from": "commander@2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" + "from": "commander@2.0.0" }, "growl": { "version": "1.8.1", @@ -2669,13 +2610,11 @@ "dependencies": { "commander": { "version": "0.6.1", - "from": "commander@0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + "from": "commander@0.6.1" }, "mkdirp": { "version": "0.3.0", - "from": "mkdirp@0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + "from": "mkdirp@0.3.0" } } }, @@ -2695,13 +2634,11 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + "from": "mkdirp@~0.3.5" }, "glob": { "version": "3.2.3", "from": "glob@3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "dependencies": { "minimatch": { "version": "0.2.14", @@ -2731,11 +2668,13 @@ }, "redis": { "version": "0.12.1", - "from": "redis@0.12.1" + "from": "redis@0.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz" }, "redis-sentinel": { "version": "0.1.1", "from": "redis-sentinel@0.1.1", + "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz", "dependencies": { "redis": { "version": "0.11.0", @@ -2743,7 +2682,8 @@ }, "q": { "version": "0.9.2", - "from": "q@0.9.2" + "from": "q@0.9.2", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.2.tgz" } } }, @@ -2797,7 +2737,6 @@ "request": { "version": "2.79.0", "from": "request@^2.69.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", "dependencies": { "aws-sign2": { "version": "0.6.0", @@ -2805,8 +2744,7 @@ }, "aws4": { "version": "1.5.0", - "from": "aws4@^1.2.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.5.0.tgz" + "from": "aws4@^1.2.1" }, "caseless": { "version": "0.11.0", @@ -2833,12 +2771,10 @@ "form-data": { "version": "2.1.2", "from": "form-data@~2.1.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz", "dependencies": { "asynckit": { "version": "0.4.0", - "from": "asynckit@^0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + "from": "asynckit@^0.4.0" } } }, @@ -2897,7 +2833,6 @@ "is-my-json-valid": { "version": "2.15.0", "from": "is-my-json-valid@^2.12.4", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", "dependencies": { "generate-function": { "version": "2.0.0", @@ -2915,8 +2850,7 @@ }, "jsonpointer": { "version": "4.0.1", - "from": "jsonpointer@^4.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz" + "from": "jsonpointer@^4.0.0" }, "xtend": { "version": "4.0.1", @@ -2969,7 +2903,6 @@ "jsprim": { "version": "1.3.1", "from": "jsprim@^1.2.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz", "dependencies": { "extsprintf": { "version": "1.0.2", @@ -2999,8 +2932,7 @@ }, "dashdash": { "version": "1.14.1", - "from": "dashdash@^1.12.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + "from": "dashdash@^1.12.0" }, "getpass": { "version": "0.1.6", @@ -3012,8 +2944,7 @@ }, "tweetnacl": { "version": "0.14.5", - "from": "tweetnacl@~0.14.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + "from": "tweetnacl@~0.14.0" }, "jodid25519": { "version": "1.0.2", @@ -3025,8 +2956,7 @@ }, "bcrypt-pbkdf": { "version": "1.0.0", - "from": "bcrypt-pbkdf@^1.0.0", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz" + "from": "bcrypt-pbkdf@^1.0.0" } } } @@ -3060,8 +2990,7 @@ }, "qs": { "version": "6.3.0", - "from": "qs@~6.3.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz" + "from": "qs@~6.3.0" }, "stringstream": { "version": "0.0.5", @@ -3070,12 +2999,10 @@ "tough-cookie": { "version": "2.3.2", "from": "tough-cookie@~2.3.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", "dependencies": { "punycode": { "version": "1.4.1", - "from": "punycode@^1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + "from": "punycode@^1.4.1" } } }, @@ -3091,8 +3018,7 @@ "dependencies": { "axo": { "version": "0.0.2", - "from": "axo@0.0.x", - "resolved": "https://registry.npmjs.org/axo/-/axo-0.0.2.tgz" + "from": "axo@0.0.x" }, "eventemitter3": { "version": "1.1.1", @@ -3136,21 +3062,30 @@ }, "rimraf": { "version": "2.2.6", - "from": "rimraf@2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz" + "from": "rimraf@2.2.6" + }, + "rolling-rate-limiter": { + "version": "0.1.4", + "from": "rolling-rate-limiter@git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master", + "resolved": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#8a1a2cd8aaf9cd1a75cc81317b7f261157be2149", + "dependencies": { + "microtime-nodejs": { + "version": "1.0.0", + "from": "microtime-nodejs@~1.0.0" + } + } }, "sanitizer": { "version": "0.1.1", "from": "sanitizer@0.1.1" }, "sequelize": { - "version": "3.29.0", + "version": "3.30.0", "from": "sequelize@^3.2.0", "dependencies": { "bluebird": { "version": "3.4.7", - "from": "bluebird@^3.3.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz" + "from": "bluebird@^3.3.4" }, "depd": { "version": "1.1.0", @@ -3166,7 +3101,7 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz" }, "inflection": { - "version": "1.10.0", + "version": "1.12.0", "from": "inflection@^1.6.0" }, "lodash": { @@ -3176,8 +3111,7 @@ }, "moment": { "version": "2.17.1", - "from": "moment@^2.13.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.1.tgz" + "from": "moment@^2.10.6" }, "moment-timezone": { "version": "0.5.11", @@ -3190,7 +3124,6 @@ "retry-as-promised": { "version": "2.2.0", "from": "retry-as-promised@^2.0.0", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.2.0.tgz", "dependencies": { "cross-env": { "version": "3.1.4", @@ -3199,12 +3132,10 @@ "cross-spawn": { "version": "3.0.1", "from": "cross-spawn@^3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", "dependencies": { "lru-cache": { "version": "4.0.2", "from": "lru-cache@^4.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", "dependencies": { "pseudomap": { "version": "1.0.2", @@ -3212,20 +3143,17 @@ }, "yallist": { "version": "2.0.0", - "from": "yallist@^2.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" + "from": "yallist@^2.0.0" } } }, "which": { "version": "1.2.12", "from": "which@^1.2.9", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.12.tgz", "dependencies": { "isexe": { "version": "1.1.2", - "from": "isexe@^1.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz" + "from": "isexe@^1.1.1" } } } @@ -3251,17 +3179,16 @@ }, "shimmer": { "version": "1.1.0", - "from": "shimmer@1.1.0" + "from": "shimmer@1.1.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz" }, "terraformer-wkt-parser": { "version": "1.1.2", "from": "terraformer-wkt-parser@^1.1.0", - "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.1.2.tgz", "dependencies": { "terraformer": { "version": "1.0.7", - "from": "terraformer@~1.0.5", - "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.7.tgz" + "from": "terraformer@~1.0.5" } } }, @@ -3271,12 +3198,12 @@ }, "validator": { "version": "5.7.0", - "from": "validator@^5.2.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-5.7.0.tgz" + "from": "validator@^5.2.0" }, "wkx": { "version": "0.2.0", - "from": "wkx@0.2.0" + "from": "wkx@0.2.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz" } } }, @@ -3287,7 +3214,8 @@ "dependencies": { "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0" + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" } } }, @@ -3301,8 +3229,7 @@ "dependencies": { "os-tmpdir": { "version": "1.0.2", - "from": "os-tmpdir@^1.0.0", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + "from": "os-tmpdir@^1.0.0" } } }, @@ -3313,8 +3240,7 @@ }, "uuid": { "version": "3.0.1", - "from": "uuid@^3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz" + "from": "uuid@^3.0.1" }, "v8-profiler": { "version": "5.6.5", @@ -3322,13 +3248,11 @@ "dependencies": { "nan": { "version": "2.5.1", - "from": "nan@^2.3.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz" + "from": "nan@^2.3.2" }, "node-pre-gyp": { "version": "0.6.32", "from": "node-pre-gyp@^0.6.5", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz", "dependencies": { "mkdirp": { "version": "0.5.1", @@ -3336,8 +3260,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "from": "minimist@0.0.8" } } }, @@ -3354,7 +3277,6 @@ "npmlog": { "version": "4.0.2", "from": "npmlog@^4.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz", "dependencies": { "are-we-there-yet": { "version": "1.1.2", @@ -3367,7 +3289,6 @@ "readable-stream": { "version": "2.2.2", "from": "readable-stream@^2.0.0 || ^1.1.13", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.2.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -3408,7 +3329,6 @@ "gauge": { "version": "2.7.2", "from": "gauge@~2.7.1", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.2.tgz", "dependencies": { "aproba": { "version": "1.0.4", @@ -3428,18 +3348,15 @@ }, "signal-exit": { "version": "3.0.2", - "from": "signal-exit@^3.0.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz" + "from": "signal-exit@^3.0.0" }, "string-width": { "version": "1.0.2", "from": "string-width@^1.0.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "dependencies": { "code-point-at": { "version": "1.1.0", - "from": "code-point-at@^1.0.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" + "from": "code-point-at@^1.0.0" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -3447,8 +3364,7 @@ "dependencies": { "number-is-nan": { "version": "1.0.1", - "from": "number-is-nan@^1.0.0", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + "from": "number-is-nan@^1.0.0" } } } @@ -3505,7 +3421,6 @@ "glob": { "version": "7.1.1", "from": "glob@^7.0.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", "dependencies": { "fs.realpath": { "version": "1.0.0", @@ -3514,7 +3429,6 @@ "inflight": { "version": "1.0.6", "from": "inflight@^1.0.4", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -3549,7 +3463,6 @@ "once": { "version": "1.4.0", "from": "once@^1.3.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "dependencies": { "wrappy": { "version": "1.0.2", @@ -3559,8 +3472,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "from": "path-is-absolute@^1.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "from": "path-is-absolute@^1.0.0" } } } @@ -3584,25 +3496,24 @@ "dependencies": { "graceful-fs": { "version": "4.1.11", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + "from": "graceful-fs@^4.1.2" } } }, "inherits": { "version": "2.0.3", - "from": "inherits@2" + "from": "inherits@~2.0.0" } } }, "tar-pack": { "version": "3.3.0", "from": "tar-pack@~3.3.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.3.0.tgz", "dependencies": { "debug": { "version": "2.2.0", "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", @@ -3616,12 +3527,11 @@ "dependencies": { "graceful-fs": { "version": "4.1.11", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz" + "from": "graceful-fs@^4.1.2" }, "inherits": { "version": "2.0.3", - "from": "inherits@2" + "from": "inherits@~2.0.0" } } }, @@ -3668,7 +3578,6 @@ "readable-stream": { "version": "2.1.5", "from": "readable-stream@~2.1.4", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", @@ -3713,6 +3622,7 @@ "xml2js": { "version": "0.2.0", "from": "xml2js@0.2.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz", "dependencies": { "sax": { "version": "1.2.1", diff --git a/services/web/package.json b/services/web/package.json index 0e3c444a75..c61b07243c 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -49,7 +49,6 @@ "passport": "^0.3.2", "passport-ldapauth": "^0.6.0", "passport-local": "^1.0.0", - "redback": "0.4.0", "redis": "0.10.1", "redis-sharelatex": "0.0.9", "request": "^2.69.0", @@ -64,13 +63,16 @@ "v8-profiler": "^5.2.3", "xml2js": "0.2.0", "passport-saml": "^0.15.0", - "uuid": "^3.0.1" + "uuid": "^3.0.1", + "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master" }, "devDependencies": { + "autoprefixer": "^6.6.1", "bunyan": "0.22.1", - "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master", "chai": "", "chai-spies": "", + "clean-css": "^3.4.18", + "es6-promise": "^4.0.5", "grunt-available-tasks": "0.4.1", "grunt-bunyan": "0.5.0", "grunt-contrib-clean": "0.5.0", @@ -79,7 +81,6 @@ "grunt-contrib-requirejs": "0.4.1", "grunt-contrib-watch": "^1.0.0", "grunt-env": "0.4.4", - "clean-css": "^3.4.18", "grunt-exec": "^0.4.7", "grunt-execute": "^0.2.2", "grunt-file-append": "0.0.6", @@ -87,9 +88,11 @@ "grunt-mocha-test": "0.9.0", "grunt-newer": "^1.2.0", "grunt-parallel": "^0.5.1", + "grunt-postcss": "^0.8.0", "grunt-sed": "^0.1.1", "sandboxed-module": "0.2.0", "sinon": "", - "timekeeper": "" + "timekeeper": "", + "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master" } } diff --git a/services/web/public/coffee/directives/expandableTextArea.coffee b/services/web/public/coffee/directives/expandableTextArea.coffee new file mode 100644 index 0000000000..8f646c10a7 --- /dev/null +++ b/services/web/public/coffee/directives/expandableTextArea.coffee @@ -0,0 +1,16 @@ +define [ + "base" +], (App) -> + App.directive "expandableTextArea", () -> + restrict: "A" + link: (scope, el) -> + resetHeight = () -> + el.css("height", "auto") + el.css("height", el.prop("scrollHeight")) + + scope.$watch (() -> el.val()), resetHeight + + resetHeight() + + + \ No newline at end of file diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index cf9e8abe66..08531f993a 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -28,6 +28,7 @@ define [ "directives/onEnter" "directives/stopPropagation" "directives/rightClick" + "directives/expandableTextArea" "services/queued-http" "filters/formatDate" "main/event" diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 9d7eca813a..1de01b5467 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -84,6 +84,9 @@ define [ setTrackingChanges: (track_changes) -> @doc.track_changes = track_changes + getTrackingChanges: () -> + !!@doc.track_changes + setTrackChangesIdSeeds: (id_seeds) -> @doc.track_changes_id_seeds = id_seeds diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 22fcef1a69..3b6672fae8 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -162,8 +162,9 @@ define [ @_syncTimeout = null want = @$scope.editor.wantTrackChanges - have = @$scope.editor.trackChanges + have = doc.getTrackingChanges() if want == have + @$scope.editor.trackChanges = want return do tryToggle = () => diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 41c70b4dee..248d8bec38 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -279,7 +279,7 @@ define [ session.setUseWrapMode(true) # use syntax validation only when explicitly set - if scope.syntaxValidation? and syntaxValidationEnabled + if scope.syntaxValidation? and syntaxValidationEnabled and !scope.fileName.match(/\.bib$/) session.setOption("useWorker", scope.syntaxValidation); # now attach session to editor diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 2d57100cc5..ed15da2958 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -35,8 +35,8 @@ define [ @$scope.$on "comment:remove", (e, comment_id) => @removeCommentId(comment_id) - @$scope.$on "comment:resolve_thread", (e, thread_id) => - @resolveCommentByThreadId(thread_id) + @$scope.$on "comment:resolve_threads", (e, thread_ids) => + @resolveCommentByThreadIds(thread_ids) @$scope.$on "comment:unresolve_thread", (e, thread_id) => @unresolveCommentByThreadId(thread_id) @@ -105,29 +105,45 @@ define [ # ace has updated @rangesTracker.on "insert:added", (change) => sl_console.log "[insert:added]", change - setTimeout () => @_onInsertAdded(change) + setTimeout () => + @_onInsertAdded(change) + @broadcastChange() @rangesTracker.on "insert:removed", (change) => sl_console.log "[insert:removed]", change - setTimeout () => @_onInsertRemoved(change) + setTimeout () => + @_onInsertRemoved(change) + @broadcastChange() @rangesTracker.on "delete:added", (change) => sl_console.log "[delete:added]", change - setTimeout () => @_onDeleteAdded(change) + setTimeout () => + @_onDeleteAdded(change) + @broadcastChange() @rangesTracker.on "delete:removed", (change) => sl_console.log "[delete:removed]", change - setTimeout () => @_onDeleteRemoved(change) + setTimeout () => + @_onDeleteRemoved(change) + @broadcastChange() @rangesTracker.on "changes:moved", (changes) => sl_console.log "[changes:moved]", changes - setTimeout () => @_onChangesMoved(changes) + setTimeout () => + @_onChangesMoved(changes) + @broadcastChange() @rangesTracker.on "comment:added", (comment) => sl_console.log "[comment:added]", comment - setTimeout () => @_onCommentAdded(comment) + setTimeout () => + @_onCommentAdded(comment) + @broadcastChange() @rangesTracker.on "comment:moved", (comment) => sl_console.log "[comment:moved]", comment - setTimeout () => @_onCommentMoved(comment) + setTimeout () => + @_onCommentMoved(comment) + @broadcastChange() @rangesTracker.on "comment:removed", (comment) => sl_console.log "[comment:removed]", comment - setTimeout () => @_onCommentRemoved(comment) + setTimeout () => + @_onCommentRemoved(comment) + @broadcastChange() @rangesTracker.on "clear", () => @clearAnnotations() @@ -150,6 +166,8 @@ define [ for comment in @rangesTracker.comments @_onCommentAdded(comment) + + @broadcastChange() addComment: (offset, content, thread_id) -> op = { c: content, p: offset, t: thread_id } @@ -190,15 +208,20 @@ define [ removeCommentId: (comment_id) -> @rangesTracker.removeCommentId(comment_id) - resolveCommentByThreadId: (thread_id) -> + resolveCommentByThreadIds: (thread_ids) -> + resolve_ids = {} + for id in thread_ids + resolve_ids[id] = true for comment in @rangesTracker?.comments or [] - if comment.op.t == thread_id + if resolve_ids[comment.op.t] @_onCommentRemoved(comment) + @broadcastChange() unresolveCommentByThreadId: (thread_id) -> for comment in @rangesTracker?.comments or [] if comment.op.t == thread_id @_onCommentAdded(comment) + @broadcastChange() checkMapping: () -> # TODO: reintroduce this check @@ -303,7 +326,6 @@ define [ background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text" callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout") @changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id } - @broadcastChange() _onDeleteAdded: (change) -> position = @_shareJsOffsetToAcePosition(change.op.p) @@ -318,7 +340,6 @@ define [ callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout") @changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id } - @broadcastChange() _onInsertRemoved: (change) -> {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id] @@ -326,7 +347,6 @@ define [ session = @editor.getSession() session.removeMarker background_marker_id session.removeMarker callout_marker_id - @broadcastChange() _onDeleteRemoved: (change) -> {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id] @@ -334,7 +354,6 @@ define [ session = @editor.getSession() session.removeMarker background_marker_id session.removeMarker callout_marker_id - @broadcastChange() _onCommentAdded: (comment) -> if @rangesTracker.resolvedThreadIds[comment.op.t] @@ -350,7 +369,6 @@ define [ background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text" callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout") @changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id } - @broadcastChange() _onCommentRemoved: (comment) -> if @changeIdToMarkerIdMap[comment.id]? @@ -360,7 +378,6 @@ define [ session = @editor.getSession() session.removeMarker background_marker_id session.removeMarker callout_marker_id - @broadcastChange() _aceRangeToShareJs: (range) -> lines = @editor.getSession().getDocument().getLines 0, range.row @@ -385,14 +402,12 @@ define [ end = start @_updateMarker(change.id, start, end) @editor.renderer.updateBackMarkers() - @broadcastChange() _onCommentMoved: (comment) -> start = @_shareJsOffsetToAcePosition(comment.op.p) end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length) @_updateMarker(comment.id, start, end) @editor.renderer.updateBackMarkers() - @broadcastChange() _updateMarker: (change_id, start, end) -> return if !@changeIdToMarkerIdMap[change_id]? diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 7a679bb6e3..e31b84f051 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -105,7 +105,6 @@ load = (EventEmitter) -> throw new Error("unknown op type") addComment: (op, metadata) -> - # TODO: Don't allow overlapping comments? @comments.push comment = { id: op.t or @newId() op: # Copy because we'll modify in place diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 723afdc648..ac687386f8 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -65,6 +65,18 @@ define [ ide.socket.on "reopen-thread", (thread_id) -> _onCommentReopened(thread_id) + + ide.socket.on "delete-thread", (thread_id) -> + _onThreadDeleted(thread_id) + $scope.$apply () -> + + ide.socket.on "edit-message", (thread_id, message_id, content) -> + _onCommentEdited(thread_id, message_id, content) + $scope.$apply () -> + + ide.socket.on "delete-message", (thread_id, message_id) -> + _onCommentDeleted(thread_id, message_id) + $scope.$apply () -> rangesTrackers = {} @@ -214,8 +226,10 @@ define [ delete delete_changes[comment.id] if $scope.reviewPanel.resolvedThreadIds[comment.op.t] new_comment = resolvedComments[comment.id] ?= {} + delete entries[comment.id] else new_comment = entries[comment.id] ?= {} + delete resolvedComments[comment.id] new_entry = { type: "comment" thread_id: comment.op.t @@ -342,31 +356,72 @@ define [ event_tracking.sendMB "rp-comment-reopen" _onCommentResolved = (thread_id, user) -> - thread = $scope.reviewPanel.commentThreads[thread_id] + thread = getThread(thread_id) + return if !thread? thread.resolved = true thread.resolved_by_user = formatUser(user) thread.resolved_at = new Date() $scope.reviewPanel.resolvedThreadIds[thread_id] = true - $scope.$broadcast "comment:resolve_thread", thread_id + $scope.$broadcast "comment:resolve_threads", [thread_id] _onCommentReopened = (thread_id) -> - thread = $scope.reviewPanel.commentThreads[thread_id] + thread = getThread(thread_id) + return if !thread? delete thread.resolved delete thread.resolved_by_user delete thread.resolved_at delete $scope.reviewPanel.resolvedThreadIds[thread_id] $scope.$broadcast "comment:unresolve_thread", thread_id - _onCommentDeleted = (thread_id) -> - if $scope.reviewPanel.resolvedThreadIds[thread_id]? - delete $scope.reviewPanel.resolvedThreadIds[thread_id] - + _onThreadDeleted = (thread_id) -> + delete $scope.reviewPanel.resolvedThreadIds[thread_id] delete $scope.reviewPanel.commentThreads[thread_id] + $scope.$broadcast "comment:remove", thread_id - $scope.deleteComment = (entry_id, thread_id) -> - _onCommentDeleted(thread_id) - $scope.$broadcast "comment:remove", entry_id + _onCommentEdited = (thread_id, comment_id, content) -> + thread = getThread(thread_id) + return if !thread? + for message in thread.messages + if message.id == comment_id + message.content = content + updateEntries() + + _onCommentDeleted = (thread_id, comment_id) -> + thread = getThread(thread_id) + return if !thread? + thread.messages = thread.messages.filter (m) -> m.id != comment_id + updateEntries() + + $scope.deleteThread = (entry_id, doc_id, thread_id) -> + _onThreadDeleted(thread_id) + $http({ + method: "DELETE" + url: "/project/#{$scope.project_id}/doc/#{doc_id}/thread/#{thread_id}", + headers: { + 'X-CSRF-Token': window.csrfToken + } + }) event_tracking.sendMB "rp-comment-delete" + + $scope.saveEdit = (thread_id, comment) -> + $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}/edit", { + content: comment.content + _csrf: window.csrfToken + }) + $timeout () -> + $scope.$broadcast "review-panel:layout" + + $scope.deleteComment = (thread_id, comment) -> + _onCommentDeleted(thread_id, comment.id) + $http({ + method: "DELETE" + url: "/project/#{$scope.project_id}/thread/#{thread_id}/messages/#{comment.id}", + headers: { + 'X-CSRF-Token': window.csrfToken + } + }) + $timeout () -> + $scope.$broadcast "review-panel:layout" $scope.setSubView = (subView) -> $scope.reviewPanel.subView = subView @@ -428,9 +483,9 @@ define [ for comment in thread.messages formatComment(comment) if thread.resolved_by_user? - $scope.$broadcast "comment:resolve_thread", thread_id thread.resolved_by_user = formatUser(thread.resolved_by_user) $scope.reviewPanel.resolvedThreadIds[thread_id] = true + $scope.$broadcast "comment:resolve_threads", [thread_id] $scope.reviewPanel.commentThreads = threads $timeout () -> $scope.$broadcast "review-panel:layout" diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index db54574d27..7c7811d553 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -11,6 +11,8 @@ define [ onResolve: "&" onReply: "&" onIndicatorClick: "&" + onSaveEdit: "&" + onDelete: "&" link: (scope, element, attrs) -> scope.state = animating: false @@ -26,4 +28,33 @@ define [ scope.state.animating = true element.find(".rp-entry").css("top", 0) $timeout((() -> scope.onResolve()), 350) - return true \ No newline at end of file + return true + + scope.startEditing = (comment) -> + comment.editing = true + setTimeout () -> + scope.$emit "review-panel:layout" + + scope.saveEdit = (comment) -> + comment.editing = false + scope.onSaveEdit({comment:comment}) + + scope.confirmDelete = (comment) -> + comment.deleting = true + setTimeout () -> + scope.$emit "review-panel:layout" + + scope.cancelDelete = (comment) -> + comment.deleting = false + setTimeout () -> + scope.$emit "review-panel:layout" + + scope.doDelete = (comment) -> + comment.deleting = false + scope.onDelete({comment: comment}) + + scope.saveEditOnEnter = (ev, comment) -> + if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey + ev.preventDefault() + scope.saveEdit(comment) + \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index fa556e2939..d500d24db8 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -31,8 +31,9 @@ define [ scope.onUnresolve({ threadId }) scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId - scope.handleDelete = (entryId, threadId) -> - scope.onDelete({ entryId, threadId }) + scope.handleDelete = (entryId, docId, threadId) -> + scope.onDelete({ entryId, docId, threadId }) + scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId getDocNameById = (docId) -> doc = _.find(scope.docs, (doc) -> doc.doc.id == docId) diff --git a/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee b/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee index 5a96bde5c1..9fdb3e9ca2 100644 --- a/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee +++ b/services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee @@ -6,4 +6,4 @@ define [ $scope.hasProjects = window.data.projects.length > 0 $scope.userHasNoSubscription = window.userHasNoSubscription - $scope.randomView = _.shuffle(["default", "dropbox", "github"])[0] + diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 431c24ada7..161598d32d 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -14,10 +14,9 @@ define [ $scope.searchText = value : "" - if $scope.projects.length == 0 - $timeout () -> - recalculateProjectListHeight() - , 10 + $timeout () -> + recalculateProjectListHeight() + , 10 recalculateProjectListHeight = () -> topOffset = $(".project-list-card")?.offset()?.top diff --git a/services/web/public/stylesheets/app/editor/file-tree.less b/services/web/public/stylesheets/app/editor/file-tree.less index 5a4d7feed1..7847822bf9 100644 --- a/services/web/public/stylesheets/app/editor/file-tree.less +++ b/services/web/public/stylesheets/app/editor/file-tree.less @@ -35,6 +35,10 @@ aside#file-tree { line-height: 2.6; position: relative; + .entity { + user-select: none; + } + .entity-name { color: @gray-darker; cursor: pointer; diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 54142f2dcc..da9fa7eb43 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -32,6 +32,7 @@ .rp-button() { + display: block; // IE doesn't do flex with inline items. background-color: @rp-highlight-blue; color: #FFF; text-align: center; @@ -119,14 +120,11 @@ .rp-size-expanded & { display: flex; align-items: center; + justify-content: space-between; padding: 0 5px; } - // .rp-state-current-file & { - // position: absolute; - // top: 0; - // left: 0; - // right: 0; - // } + + position: relative; height: @rp-toolbar-height; border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; @@ -338,6 +336,9 @@ .rp-entry-details { line-height: 1.4; margin-left: 5px; + // We need to set any low-enough flex base size (0px), making it growable (1) and non-shrinkable (0). + // This is needed to ensure that IE makes the element fill the available space. + flex: 1 0 1px; .rp-state-overview & { margin-left: 0; @@ -351,6 +352,9 @@ font-weight: @rp-semibold-weight; font-style: normal; } + .rp-comment-actions { + a { color: @rp-type-blue; } + } .rp-content-highlight { color: @rp-type-darkgrey; @@ -414,12 +418,6 @@ margin: 0; color: @rp-type-darkgrey; } - - .rp-comment-metadata { - color: @rp-type-blue; - font-size: @rp-small-font-size; - margin: 0; - } .rp-comment-resolver { color: @rp-type-blue; @@ -452,6 +450,7 @@ border: solid 1px @rp-border-grey; resize: vertical; color: @rp-type-darkgrey; + margin-top: 3px; } .rp-icon-delete { @@ -592,6 +591,7 @@ z-index: 2; } .rp-nav-item { + display: block; color: lighten(@rp-type-blue, 25%); flex: 0 0 50%; border-top: solid 3px transparent; @@ -790,7 +790,7 @@ position: absolute; width: 300px; left: -150px; - max-height: 90%; + max-height: ~"calc(100vh - 100px)"; margin-top: @rp-entry-arrow-width * 1.5; margin-left: 1em; background-color: @rp-bg-blue; @@ -813,7 +813,9 @@ } } .resolved-comments-scroller { - flex: 0 0 100%; + flex: 0 0 auto; // Can't use 100% in the flex-basis key here, IE won't account for padding. + width: 100%; // We need to set the width explicitly, as flex-basis won't work. + max-height: ~"calc(100vh - 100px)"; // We also need to explicitly set the max-height, IE won't compute the flex-determined height. padding: 5px; overflow-y: auto; } @@ -841,6 +843,7 @@ border-bottom-left-radius: 3px; font-size: 10px; z-index: 2; + white-space: nowrap; &.rp-track-changes-indicator-on-dark { background-color: rgba(88, 88, 88, .8); @@ -865,3 +868,10 @@ display: block; } } + +// Helper class for elements which aren't treated as flex-items by IE10, e.g: +// * inline items; +// * unknown elements (elements which aren't standard DOM elements, such as custom element directives) +.rp-flex-block { + display: block; +} diff --git a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee index 49e8292f97..daa0da0531 100644 --- a/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Announcement/AnnouncementsHandlerTests.coffee @@ -10,18 +10,21 @@ expect = require("chai").expect describe 'AnnouncementsHandler', -> beforeEach -> - @user_id = "some_id" + @user = + _id:"some_id" + email: "someone@gmail.com" @AnalyticsManager = getLastOccurance: sinon.stub() @BlogHandler = getLatestAnnouncements:sinon.stub() + @settings = {} @handler = SandboxedModule.require modulePath, requires: "../Analytics/AnalyticsManager":@AnalyticsManager "../Blog/BlogHandler":@BlogHandler + "settings-sharelatex":@settings "logger-sharelatex": log:-> - describe "getUnreadAnnouncements", -> beforeEach -> @stubbedAnnouncements = [ @@ -44,7 +47,7 @@ describe 'AnnouncementsHandler', -> it "should mark all announcements as read is false", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user_id, (err, announcements)=> + @handler.getUnreadAnnouncements @user, (err, announcements)=> announcements[0].read.should.equal false announcements[1].read.should.equal false announcements[2].read.should.equal false @@ -53,7 +56,7 @@ describe 'AnnouncementsHandler', -> it "should should be sorted again to ensure correct order", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, []) - @handler.getUnreadAnnouncements @user_id, (err, announcements)=> + @handler.getUnreadAnnouncements @user, (err, announcements)=> announcements[3].should.equal @stubbedAnnouncements[2] announcements[2].should.equal @stubbedAnnouncements[3] announcements[1].should.equal @stubbedAnnouncements[1] @@ -62,7 +65,7 @@ describe 'AnnouncementsHandler', -> it "should return older ones marked as read as well", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2014/04/12/title-date-irrelivant"}}) - @handler.getUnreadAnnouncements @user_id, (err, announcements)=> + @handler.getUnreadAnnouncements @user, (err, announcements)=> announcements[0].id.should.equal @stubbedAnnouncements[0].id announcements[0].read.should.equal false @@ -79,7 +82,7 @@ describe 'AnnouncementsHandler', -> it "should return all of them marked as read", (done)-> @AnalyticsManager.getLastOccurance.callsArgWith(2, null, {segmentation:{blogPostId:"/2016/11/01/introducting-latex-code-checker"}}) - @handler.getUnreadAnnouncements @user_id, (err, announcements)=> + @handler.getUnreadAnnouncements @user, (err, announcements)=> announcements[0].read.should.equal true announcements[1].read.should.equal true announcements[2].read.should.equal true @@ -87,3 +90,70 @@ describe 'AnnouncementsHandler', -> done() + describe "with custom domain announcements", -> + beforeEach -> + @stubbedDomainSpecificAnn = [ + { + domains: ["gmail.com", 'yahoo.edu'] + title: "some message" + excerpt: "read this" + url:"http://www.sharelatex.com/i/somewhere" + id:"iaaa" + date: new Date(1308369600000).toString() + } + ] + + @handler._domainSpecificAnnouncements = sinon.stub().returns(@stubbedDomainSpecificAnn) + + it "should insert the domain specific in the correct place", (done)-> + @AnalyticsManager.getLastOccurance.callsArgWith(2, null, []) + @handler.getUnreadAnnouncements @user, (err, announcements)=> + announcements[4].should.equal @stubbedAnnouncements[2] + announcements[3].should.equal @stubbedAnnouncements[3] + announcements[2].should.equal @stubbedAnnouncements[1] + announcements[1].should.equal @stubbedDomainSpecificAnn[0] + announcements[0].should.equal @stubbedAnnouncements[0] + done() + + describe "_domainSpecificAnnouncements", -> + beforeEach -> + @settings.domainAnnouncements = [ + { + domains: ["gmail.com", 'yahoo.edu'] + title: "some message" + excerpt: "read this" + url:"http://www.sharelatex.com/i/somewhere" + id:"id1" + date: new Date(1308369600000).toString() + }, { + domains: ["gmail.com", 'yahoo.edu'] + title: "some message" + excerpt: "read this" + url:"http://www.sharelatex.com/i/somewhere" + date: new Date(1308369600000).toString() + }, { + domains: ["gmail.com", 'yahoo.edu'] + title: "some message" + excerpt: "read this" + url:"http://www.sharelatex.com/i/somewhere" + id:"id3" + date: new Date(1308369600000).toString() + } + ] + + it "should filter announcments which don't have an id", (done) -> + result = @handler._domainSpecificAnnouncements "someone@gmail.com" + result.length.should.equal 2 + result[0].id.should.equal "id1" + result[1].id.should.equal "id3" + done() + + + it "should match on domain", (done) -> + @settings.domainAnnouncements[2].domains = ["yahoo.com"] + result = @handler._domainSpecificAnnouncements "someone@gmail.com" + result.length.should.equal 1 + result[0].id.should.equal "id1" + done() + + diff --git a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee index cbc24bca1f..e55f0d04da 100644 --- a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee @@ -23,6 +23,7 @@ describe "CommentsController", -> '../Authentication/AuthenticationController': @AuthenticationController '../User/UserInfoManager': @UserInfoManager = {} '../User/UserInfoController': @UserInfoController = {} + "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {} @req = {} @res = json: sinon.stub() @@ -134,6 +135,80 @@ describe "CommentsController", -> it "should return a success code", -> @res.send.calledWith(204).should.equal + describe "deleteThread", -> + beforeEach -> + @req.params = + project_id: @project_id = "mock-project-id" + doc_id: @doc_id = "mock-doc-id" + thread_id: @thread_id = "mock-thread-id" + @DocumentUpdaterHandler.deleteThread = sinon.stub().yields() + @ChatApiHandler.deleteThread = sinon.stub().yields() + @CommentsController.deleteThread @req, @res + + it "should ask the doc udpater to delete the thread", -> + @DocumentUpdaterHandler.deleteThread + .calledWith(@project_id, @doc_id, @thread_id) + .should.equal true + + it "should ask the chat handler to delete the thread", -> + @ChatApiHandler.deleteThread + .calledWith(@project_id, @thread_id) + .should.equal true + + it "should tell the client the thread was deleted", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "delete-thread", @thread_id) + .should.equal true + + it "should return a success code", -> + @res.send.calledWith(204).should.equal + + describe "editMessage", -> + beforeEach -> + @req.params = + project_id: @project_id = "mock-project-id" + thread_id: @thread_id = "mock-thread-id" + message_id: @message_id = "mock-thread-id" + @req.body = + content: @content = "mock-content" + @ChatApiHandler.editMessage = sinon.stub().yields() + @CommentsController.editMessage @req, @res + + it "should ask the chat handler to edit the comment", -> + @ChatApiHandler.editMessage + .calledWith(@project_id, @thread_id, @message_id, @content) + .should.equal true + + it "should tell the client the comment was edited", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "edit-message", @thread_id, @message_id, @content) + .should.equal true + + it "should return a success code", -> + @res.send.calledWith(204).should.equal + + describe "deleteMessage", -> + beforeEach -> + @req.params = + project_id: @project_id = "mock-project-id" + thread_id: @thread_id = "mock-thread-id" + message_id: @message_id = "mock-thread-id" + @ChatApiHandler.deleteMessage = sinon.stub().yields() + @CommentsController.deleteMessage @req, @res + + it "should ask the chat handler to deleted the message", -> + @ChatApiHandler.deleteMessage + .calledWith(@project_id, @thread_id, @message_id) + .should.equal true + + it "should tell the client the message was deleted", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "delete-message", @thread_id, @message_id) + .should.equal true + + it "should return a success code", -> + @res.send.calledWith(204).should.equal + describe "_injectUserInfoIntoThreads", -> beforeEach -> @users = { diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee index 3bde5e991a..681915abc6 100644 --- a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee @@ -330,3 +330,38 @@ describe 'DocumentUpdaterHandler', -> @callback .calledWith(new Error("doc updater returned failure status code: 500")) .should.equal true + + describe "deleteThread", -> + beforeEach -> + @thread_id = "mock-thread-id-1" + @callback = sinon.stub() + + describe "successfully", -> + beforeEach -> + @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) + @handler.deleteThread @project_id, @doc_id, @thread_id, @callback + + it 'should delete the thread in the document updater', -> + url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/comment/#{@thread_id}" + @request.del.calledWith(url).should.equal true + + it "should call the callback", -> + @callback.calledWith(null).should.equal true + + describe "when the document updater API returns an error", -> + beforeEach -> + @request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) + @handler.deleteThread @project_id, @doc_id, @thread_id, @callback + + it "should return an error to the callback", -> + @callback.calledWith(@error).should.equal true + + describe "when the document updater returns a failure error code", -> + beforeEach -> + @request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") + @handler.deleteThread @project_id, @doc_id, @thread_id, @callback + + it "should return the callback with an error", -> + @callback + .calledWith(new Error("doc updater returned failure status code: 500")) + .should.equal true \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee b/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee index 2c4dc59262..bbb4dcd675 100644 --- a/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee +++ b/services/web/test/UnitTests/coffee/Security/LoginRateLimiterTests.coffee @@ -1,78 +1,74 @@ SandboxedModule = require('sandboxed-module') sinon = require('sinon') require('chai').should() +expect = require('chai').expect modulePath = require('path').join __dirname, '../../../../app/js/Features/Security/LoginRateLimiter' -buildKey = (k)-> - return "LoginRateLimit:#{k}" describe "LoginRateLimiter", -> + beforeEach -> @email = "bob@bob.com" - @incrStub = sinon.stub() - @getStub = sinon.stub() - @execStub = sinon.stub() - @expireStub = sinon.stub() - @delStub = sinon.stub().callsArgWith(1) - - @rclient = - auth:-> - del: @delStub - multi: => - incr: @incrStub - expire: @expireStub - get: @getStub - exec: @execStub + @RateLimiter = + clearRateLimit: sinon.stub() + addCount: sinon.stub() @LoginRateLimiter = SandboxedModule.require modulePath, requires: - 'redis-sharelatex' : createClient: () => @rclient - "settings-sharelatex":{redis:{}} - + '../../infrastructure/RateLimiter': @RateLimiter + describe "processLoginRequest", -> - it "should inc the counter for login requests in redis", (done)-> - @execStub.callsArgWith(0, "null", ["",""]) - @LoginRateLimiter.processLoginRequest @email, => - @incrStub.calledWith(buildKey(@email)).should.equal true + beforeEach -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + + it 'should call RateLimiter.addCount', (done) -> + @LoginRateLimiter.processLoginRequest @email, (err, allow) => + @RateLimiter.addCount.callCount.should.equal 1 + expect(@RateLimiter.addCount.lastCall.args[0].endpointName).to.equal 'login' + expect(@RateLimiter.addCount.lastCall.args[0].subjectName).to.equal @email done() - it "should set a expire", (done)-> - @execStub.callsArgWith(0, "null", ["",""]) - @LoginRateLimiter.processLoginRequest @email, => - @expireStub.calledWith(buildKey(@email), 60 * 2).should.equal true - done() + describe 'when login is allowed', -> - it "should return true if the count is below 10", (done)-> - @execStub.callsArgWith(0, "null", ["", 9]) - @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=> - isAllowed.should.equal true - done() + beforeEach -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) - it "should return true if the count is 10", (done)-> - @execStub.callsArgWith(0, "null", ["", 10]) - @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=> - isAllowed.should.equal true - done() + it 'should call pass allow=true', (done) -> + @LoginRateLimiter.processLoginRequest @email, (err, allow) => + expect(err).to.equal null + expect(allow).to.equal true + done() - it "should return false if the count is above 10", (done)-> - @execStub.callsArgWith(0, "null", ["", 11]) - @LoginRateLimiter.processLoginRequest @email, (err, isAllowed)=> - isAllowed.should.equal false - done() + describe 'when login is blocked', -> + beforeEach -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) - describe "smoke test user", -> - - it "should have a higher limit", (done)-> - done() + it 'should call pass allow=false', (done) -> + @LoginRateLimiter.processLoginRequest @email, (err, allow) => + expect(err).to.equal null + expect(allow).to.equal false + done() + describe 'when addCount produces an error', -> + beforeEach -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, new Error('woops')) + it 'should produce an error', (done) -> + @LoginRateLimiter.processLoginRequest @email, (err, allow) => + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done() describe "recordSuccessfulLogin", -> - it "should delete the user key", (done)-> + beforeEach -> + @RateLimiter.clearRateLimit = sinon.stub().callsArgWith 2, null + + it "should call clearRateLimit", (done)-> @LoginRateLimiter.recordSuccessfulLogin @email, => - @delStub.calledWith(buildKey(@email)).should.equal true - done() \ No newline at end of file + @RateLimiter.clearRateLimit.callCount.should.equal 1 + @RateLimiter.clearRateLimit.calledWith('login', @email).should.equal true + done() diff --git a/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee index 21c0d96f4f..06efac5d8b 100644 --- a/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee +++ b/services/web/test/UnitTests/coffee/infrastructure/RateLimterTests.coffee @@ -6,7 +6,7 @@ expect = chai.expect modulePath = "../../../../app/js/infrastructure/RateLimiter.js" SandboxedModule = require('sandboxed-module') -describe "FileStoreHandler", -> +describe "RateLimiter", -> beforeEach -> @settings = @@ -15,23 +15,27 @@ describe "FileStoreHandler", -> port:"1234" host:"somewhere" password: "password" - @redbackInstance = - addCount: sinon.stub() + @rclient = + incr: sinon.stub() + get: sinon.stub() + expire: sinon.stub() + exec: sinon.stub() + @rclient.multi = sinon.stub().returns(@rclient) + @RedisWrapper = + client: sinon.stub().returns(@rclient) - @redback = - createRateLimit: sinon.stub().returns(@redbackInstance) - @redis = - createClient: -> - return auth:-> + @limiterFn = sinon.stub() + @RollingRateLimiter = (opts) => + return @limiterFn @limiter = SandboxedModule.require modulePath, requires: + "rolling-rate-limiter": @RollingRateLimiter "settings-sharelatex":@settings "logger-sharelatex" : @logger = {log:sinon.stub(), err:sinon.stub()} - "redis-sharelatex": @redis - "redback": use: => @redback + "./RedisWrapper": @RedisWrapper @endpointName = "compiles" - @subject = "some project id" + @subject = "some-project-id" @timeInterval = 20 @throttleLimit = 5 @@ -40,43 +44,48 @@ describe "FileStoreHandler", -> subjectName: @subject throttle: @throttleLimit timeInterval: @timeInterval + @key = "RateLimiter:#{@endpointName}:{#{@subject}}" - describe "addCount", -> + + + describe 'when action is permitted', -> beforeEach -> - @redbackInstance.addCount.callsArgWith(2, null, 10) + @limiterFn = sinon.stub().callsArgWith(1, null, 0, 22) - it "should use correct namespace", (done)-> - @limiter.addCount @details, => - @redback.createRateLimit.calledWith(@endpointName).should.equal true + it 'should not produce and error', (done) -> + @limiter.addCount {}, (err, should) -> + expect(err).to.equal null done() - it "should only call it once", (done)-> - @limiter.addCount @details, => - @redbackInstance.addCount.callCount.should.equal 1 + it 'should callback with true', (done) -> + @limiter.addCount {}, (err, should) -> + expect(should).to.equal true done() - it "should use the subjectName", (done)-> - @limiter.addCount @details, => - @redbackInstance.addCount.calledWith(@details.subjectName, @details.timeInterval).should.equal true + describe 'when action is not permitted', -> + + beforeEach -> + @limiterFn = sinon.stub().callsArgWith(1, null, 4000, 0) + + it 'should not produce and error', (done) -> + @limiter.addCount {}, (err, should) -> + expect(err).to.equal null done() - it "should return true if the count is less than throttle", (done)-> - @details.throttle = 100 - @limiter.addCount @details, (err, canProcess)=> - canProcess.should.equal true + it 'should callback with false', (done) -> + @limiter.addCount {}, (err, should) -> + expect(should).to.equal false done() - it "should return true if the count is less than throttle", (done)-> - @details.throttle = 1 - @limiter.addCount @details, (err, canProcess)=> - canProcess.should.equal false - done() + describe 'when limiter produces an error', -> - it "should return false if the limit is matched", (done)-> - @details.throttle = 10 - @limiter.addCount @details, (err, canProcess)=> - canProcess.should.equal false - done() + beforeEach -> + @limiterFn = sinon.stub().callsArgWith(1, new Error('woops')) + it 'should produce and error', (done) -> + @limiter.addCount {}, (err, should) -> + expect(err).to.not.equal null + expect(err).to.be.instanceof Error + done() diff --git a/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee b/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee new file mode 100644 index 0000000000..83ea202dcd --- /dev/null +++ b/services/web/test/UnitTests/coffee/infrastructure/RedisWrapperTests.coffee @@ -0,0 +1,66 @@ +assert = require("chai").assert +sinon = require('sinon') +chai = require('chai') +should = chai.should() +expect = chai.expect +modulePath = "../../../../app/js/infrastructure/RedisWrapper.js" +SandboxedModule = require('sandboxed-module') + +describe 'RedisWrapper', -> + + beforeEach -> + @featureName = 'somefeature' + @settings = + redis: + web: + port:"1234" + host:"somewhere" + password: "password" + somefeature: {} + @normalRedisInstance = + thisIsANormalRedisInstance: true + n: 1 + @clusterRedisInstance = + thisIsAClusterRedisInstance: true + n: 2 + @redis = + createClient: sinon.stub().returns(@normalRedisInstance) + @ioredis = + Cluster: sinon.stub().returns(@clusterRedisInstance) + @logger = {log: sinon.stub()} + + @RedisWrapper = SandboxedModule.require modulePath, requires: + 'logger-sharelatex': @logger + 'settings-sharelatex': @settings + 'redis-sharelatex': @redis + 'ioredis': @ioredis + + describe 'client', -> + + beforeEach -> + @call = () => + @RedisWrapper.client(@featureName) + + describe 'when feature uses cluster', -> + + beforeEach -> + @settings.redis.somefeature = + cluster: [1, 2, 3] + + it 'should return a cluster client', -> + client = @call() + expect(client).to.equal @clusterRedisInstance + expect(client.__is_redis_cluster).to.equal true + + describe 'when feature uses normal redis', -> + + beforeEach -> + @settings.redis.somefeature = + port:"1234" + host:"somewhere" + password: "password" + + it 'should return a regular redis client', -> + client = @call() + expect(client).to.equal @normalRedisInstance + expect(client.__is_redis_cluster).to.equal undefined diff --git a/services/web/test/acceptance/coffee/RegistrationTests.coffee b/services/web/test/acceptance/coffee/RegistrationTests.coffee index 2bf96f86de..20ea0a31b1 100644 --- a/services/web/test/acceptance/coffee/RegistrationTests.coffee +++ b/services/web/test/acceptance/coffee/RegistrationTests.coffee @@ -1,9 +1,11 @@ expect = require("chai").expect +assert = require("chai").assert async = require("async") User = require "./helpers/User" request = require "./helpers/request" settings = require "settings-sharelatex" redis = require "./helpers/redis" +_ = require 'lodash' @@ -32,6 +34,41 @@ tryLoginThroughRegistrationForm = (user, email, password, callback=(err, respons }, callback +describe "LoginRateLimit", -> + + before -> + @user = new User() + @badEmail = 'bademail@example.com' + @badPassword = 'badpassword' + + it 'should rate limit login attempts after 10 within two minutes', (done) -> + @user.request.get '/login', (err, res, body) => + async.timesSeries( + 15 + , (n, cb) => + @user.getCsrfToken (error) => + return cb(error) if error? + @user.request.post { + url: "/login" + json: + email: @badEmail + password: @badPassword + }, (err, response, body) => + cb(null, body?.message?.text) + , (err, results) => + # ten incorrect-credentials messages, then five rate-limit messages + expect(results.length).to.equal 15 + assert.deepEqual( + results, + _.concat( + _.fill([1..10], 'Your email or password is incorrect. Please try again'), + _.fill([1..5], 'This account has had too many login requests. Please wait 2 minutes before trying to log in again') + ) + ) + done() + ) + + describe "LoginViaRegistration", -> before (done) ->