diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 6c9bb0ede0..f2ce93f671 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/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 460b62da1d..1fde81f5c9 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController") NotificationsBuilder = require("../Notifications/NotificationsBuilder") AnalyticsManger = require("../Analytics/AnalyticsManager") AuthenticationController = require("../Authentication/AuthenticationController") +rateLimiter = require("../../infrastructure/RateLimiter") module.exports = CollaboratorsInviteController = @@ -22,7 +23,7 @@ module.exports = CollaboratorsInviteController = return next(err) res.json({invites: invites}) - _checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) -> + _checkShouldInviteEmail: (sendingUser, email, callback=(err, shouldAllowInvite)->) -> if Settings.restrictInvitesToExistingAccounts == true logger.log {email}, "checking if user exists with this email" UserGetter.getUser {email: email}, {_id: 1}, (err, user) -> @@ -30,7 +31,19 @@ module.exports = CollaboratorsInviteController = userExists = user? and user?._id? callback(null, userExists) else - callback(null, true) + UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)-> + if err? + return callback(err) + collabLimit = user?.features?.collaborators || 1 + if collabLimit == -1 + collabLimit = 20 + collabLimit = collabLimit * 10 + opts = + endpointName: "invite_to_project" + timeInterval: 60 * 30 + subjectName: sendingUser._id + throttle: collabLimit + rateLimiter.addCount opts, callback inviteToProject: (req, res, next) -> projectId = req.params.Project_id @@ -51,7 +64,7 @@ module.exports = CollaboratorsInviteController = if !email? or email == "" logger.log {projectId, email, sendingUserId}, "invalid email address" return res.sendStatus(400) - CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> + CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)-> if err? logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" return next(err) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 4c7cc8c76a..8b130d27db 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -24,7 +24,13 @@ module.exports = RateLimiterMiddlewear.rateLimit({ endpointName: "invite-to-project" params: ["Project_id"] - maxRequests: 200 + maxRequests: 100 + timeInterval: 60 * 10 + }), + RateLimiterMiddlewear.rateLimit({ + endpointName: "invite-to-project-ip" + ipOnly:true + maxRequests: 100 timeInterval: 60 * 10 }), AuthenticationController.requireLogin(), 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/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 306aad3d2a..5360adb7a8 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -97,7 +97,7 @@ Thank you templates.projectInvite = - subject: _.template "<%= project.name %> - shared by <%= owner.email %>" + subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>" layout: BaseWithHeaderEmailLayout type:"notification" plainTextTemplate: _.template """ @@ -111,20 +111,18 @@ Thank you """ compiledTemplate: (opts) -> SingleCTAEmailBody({ - title: "#{ opts.project.name } – shared by #{ opts.owner.email }" + title: "#{ opts.project.name.slice(0, 40) } – shared by #{ opts.owner.email }" greeting: "Hi," - message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” with you." + message: "#{ opts.owner.email } wants to share “#{ opts.project.name.slice(0, 40) }” with you." secondaryMessage: null ctaText: "View project" ctaURL: opts.inviteUrl gmailGoToAction: target: opts.inviteUrl name: "View project" - description: "Join #{ opts.project.name } at ShareLaTeX" + description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX" }) - - templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" layout: BaseWithHeaderEmailLayout @@ -149,6 +147,30 @@ Thank You gmailGoToAction: null }) + +templates.testEmail = + subject: _.template "A Test Email from ShareLaTeX" + layout: BaseWithHeaderEmailLayout + type:"notification" + plainTextTemplate: _.template """ +Hi, + +This is a test email sent from ShareLaTeX. + +#{settings.appName} - <%= siteUrl %> +""" + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "A Test Email from ShareLaTeX" + greeting: "Hi," + message: "This is a test email sent from ShareLaTeX" + secondaryMessage: null + ctaText: "Open ShareLaTeX" + ctaURL: "/" + gmailGoToAction: null + }) + + module.exports = templates: templates @@ -163,4 +185,4 @@ module.exports = html: template.layout(opts) text: template?.plainTextTemplate?(opts) type:template.type - } \ No newline at end of file + } 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/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee index f486e94493..04b81581bf 100644 --- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee +++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee @@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear = user_id = AuthenticationController.getLoggedInUserId(req) || req.ip params = (opts.params or []).map (p) -> req.params[p] params.push user_id + subjectName = params.join(":") + if opts.ipOnly + subjectName = req.ip if !opts.endpointName? throw new Error("no endpointName provided") options = { endpointName: opts.endpointName timeInterval: opts.timeInterval or 60 - subjectName: params.join(":") + subjectName: subjectName throttle: opts.maxRequests or 6 } RateLimiter.addCount options, (error, canContinue)-> 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/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index 5e20c2b515..bce0befe22 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController') logger = require('logger-sharelatex') Settings = require('settings-sharelatex') FileTypeManager = require('../Uploads/FileTypeManager') -uuid = require('node-uuid') +uuid = require('uuid') fs = require('fs') module.exports = 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 3a152b045d..2218b72ecb 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/models/User.coffee b/services/web/app/coffee/models/User.coffee index c6e469a1f4..44cea29a70 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -2,7 +2,7 @@ Project = require('./Project').Project Settings = require 'settings-sharelatex' _ = require('underscore') mongoose = require('mongoose') -uuid = require('node-uuid') +uuid = require('uuid') Schema = mongoose.Schema ObjectId = Schema.ObjectId 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.pug b/services/web/app/views/project/editor/editor.pug index e02b9ec3d0..bcb778fda4 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -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.pug b/services/web/app/views/project/editor/review-panel.pug index 0419884502..590c1a6776 100644 --- a/services/web/app/views/project/editor/review-panel.pug +++ b/services/web/app/views/project/editor/review-panel.pug @@ -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.pug b/services/web/app/views/project/list.pug index d9fbf0e6b1..f707cd9411 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -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 c9616495f9..7f6d37910e 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", @@ -170,7 +170,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", @@ -251,7 +250,6 @@ "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", @@ -259,18 +257,19 @@ }, "setprototypeof": { "version": "1.0.2", - "from": "setprototypeof@1.0.2" + "from": "setprototypeof@1.0.2", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz" }, "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.15" + "from": "iconv-lite@0.4.15", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz" }, "on-finished": { "version": "2.3.0", @@ -284,7 +283,8 @@ }, "qs": { "version": "6.2.1", - "from": "qs@6.2.1" + "from": "qs@6.2.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz" }, "raw-body": { "version": "2.2.0", @@ -299,7 +299,6 @@ "type-is": { "version": "1.6.14", "from": "type-is@~1.6.14", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.14.tgz", "dependencies": { "media-typer": { "version": "0.3.0", @@ -346,13 +345,13 @@ "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", - "from": "redis-parser@^2.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.3.0.tgz" + "version": "2.4.0", + "from": "redis-parser@^2.0.0" } } } @@ -361,24 +360,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", @@ -402,8 +397,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" } } }, @@ -413,8 +407,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" } } }, @@ -451,24 +444,24 @@ "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", + "from": "uid-safe@~2.1.3", "dependencies": { "random-bytes": { "version": "1.0.0", @@ -481,7 +474,6 @@ "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", @@ -489,12 +481,12 @@ }, "setprototypeof": { "version": "1.0.2", - "from": "setprototypeof@1.0.2" + "from": "setprototypeof@1.0.2", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz" }, "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" } } } @@ -507,6 +499,7 @@ "express": { "version": "4.13.0", "from": "express@4.13.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz", "dependencies": { "accepts": { "version": "1.2.13", @@ -712,8 +705,7 @@ }, "type-is": { "version": "1.6.14", - "from": "type-is@~1.6.3", - "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", @@ -742,7 +734,7 @@ } }, "express-session": { - "version": "1.14.2", + "version": "1.15.0", "from": "express-session@^1.14.2", "dependencies": { "cookie": { @@ -754,16 +746,17 @@ "from": "cookie-signature@1.0.6" }, "crc": { - "version": "3.4.1", - "from": "crc@3.4.1" + "version": "3.4.4", + "from": "crc@3.4.4", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz" }, "debug": { - "version": "2.2.0", - "from": "debug@~2.2.0", + "version": "2.6.0", + "from": "debug@*", "dependencies": { "ms": { - "version": "0.7.1", - "from": "ms@0.7.1" + "version": "0.7.2", + "from": "ms@0.7.2" } } }, @@ -773,12 +766,11 @@ }, "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", - "from": "parseurl@~1.3.1" + "from": "parseurl@~1.3.0" }, "uid-safe": { "version": "2.1.3", @@ -786,8 +778,7 @@ "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", @@ -819,7 +810,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", @@ -835,7 +827,7 @@ "dependencies": { "inherits": { "version": "2.0.3", - "from": "inherits@~2.0.1" + "from": "inherits@2" }, "minimatch": { "version": "0.3.0", @@ -984,7 +976,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", @@ -1002,8 +993,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", @@ -1011,7 +1001,7 @@ }, "debug": { "version": "2.6.0", - "from": "debug@^2.2.0", + "from": "debug@*", "dependencies": { "ms": { "version": "0.7.2", @@ -1025,12 +1015,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", @@ -1088,8 +1078,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" } } }, @@ -1161,8 +1150,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" } } }, @@ -1184,7 +1172,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", @@ -1202,7 +1191,7 @@ "dependencies": { "uglify-js": { "version": "2.4.24", - "from": "uglify-js@~2.4.0", + "from": "uglify-js@~2.4.12", "dependencies": { "async": { "version": "0.2.10", @@ -1214,8 +1203,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" } } }, @@ -1237,7 +1225,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", @@ -1254,7 +1243,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", @@ -1267,17 +1255,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.0", - "from": "nan@^2.3.3", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.0.tgz" + "version": "2.5.1", + "from": "nan@^2.3.3" } } }, @@ -1310,7 +1295,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", @@ -1344,8 +1328,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" } } } @@ -1359,20 +1342,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", @@ -1393,7 +1373,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", @@ -1404,7 +1383,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", @@ -1421,28 +1399,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.0", - "from": "nan@^2.3.3", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.0.tgz" + "version": "2.5.1", + "from": "nan@^2.0.8" } } } @@ -1466,9 +1440,8 @@ "from": "dtrace-provider@~0.6", "dependencies": { "nan": { - "version": "2.5.0", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.0.tgz" + "version": "2.5.1", + "from": "nan@^2.0.8" } } }, @@ -1501,7 +1474,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", @@ -1536,7 +1508,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", @@ -1546,8 +1517,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" } } } @@ -1563,7 +1533,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", @@ -1577,6 +1548,10 @@ "version": "0.0.3", "from": "lsmod@~0.0.3" }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@~1.4.1" + }, "stack-trace": { "version": "0.0.7", "from": "stack-trace@0.0.7" @@ -1606,7 +1581,6 @@ "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", @@ -1639,7 +1613,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" } } }, @@ -1653,7 +1628,8 @@ "dependencies": { "iconv-lite": { "version": "0.4.15", - "from": "iconv-lite@~0.4.13" + "from": "iconv-lite@~0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz" } } }, @@ -1678,16 +1654,15 @@ "jade": { "version": "0.26.3", "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "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" } } }, @@ -1707,7 +1682,7 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5" + "from": "mkdirp@~0.3.5" }, "glob": { "version": "3.2.3", @@ -1742,6 +1717,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", @@ -1749,7 +1725,7 @@ }, "readable-stream": { "version": "1.1.14", - "from": "readable-stream@~1.1.9", + "from": "readable-stream@1.1.x", "dependencies": { "core-util-is": { "version": "1.0.2", @@ -1757,7 +1733,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", @@ -1772,6 +1749,7 @@ "mongodb": { "version": "1.4.32", "from": "mongodb@1.4.32", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz", "dependencies": { "bson": { "version": "0.2.22", @@ -1786,6 +1764,7 @@ "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", @@ -1796,7 +1775,6 @@ "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", @@ -1883,7 +1861,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", @@ -1896,7 +1873,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", @@ -1904,7 +1880,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", @@ -1932,8 +1909,7 @@ "dependencies": { "bluebird": { "version": "2.9.26", - "from": "bluebird@2.9.26", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz" + "from": "bluebird@2.9.26" }, "debug": { "version": "2.2.0", @@ -1993,7 +1969,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", @@ -2001,7 +1978,7 @@ }, "inherits": { "version": "2.0.3", - "from": "inherits@~2.0.1" + "from": "inherits@2" } } } @@ -2037,10 +2014,6 @@ } } }, - "node-uuid": { - "version": "1.4.1", - "from": "node-uuid@1.4.1" - }, "nodemailer": { "version": "2.1.0", "from": "nodemailer@2.1.0", @@ -2176,12 +2149,10 @@ "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.26", + "version": "2.9.0", "from": "aws-sdk@^2.6.12", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.7.26.tgz", "dependencies": { "buffer": { "version": "4.9.1", @@ -2189,13 +2160,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", @@ -2213,8 +2182,7 @@ }, "querystring": { "version": "0.2.0", - "from": "querystring@0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" + "from": "querystring@0.2.0" }, "sax": { "version": "1.1.5", @@ -2232,13 +2200,11 @@ }, "uuid": { "version": "3.0.0", - "from": "uuid@3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.0.tgz" + "from": "uuid@3.0.0" }, "xml2js": { "version": "0.4.15", - "from": "xml2js@0.4.15", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz" + "from": "xml2js@0.4.15" }, "xmlbuilder": { "version": "2.6.2", @@ -2351,7 +2317,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", @@ -2359,20 +2324,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", @@ -2383,7 +2345,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", @@ -2406,16 +2367,19 @@ } }, "pug": { - "version": "2.0.0-beta6", + "version": "2.0.0-beta9", "from": "pug@^2.0.0-beta6", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.0-beta9.tgz", "dependencies": { "pug-code-gen": { - "version": "1.1.0", - "from": "pug-code-gen@^1.1.0", + "version": "1.1.1", + "from": "pug-code-gen@^1.1.1", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-1.1.1.tgz", "dependencies": { "constantinople": { "version": "3.1.0", "from": "constantinople@^3.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.0.tgz", "dependencies": { "acorn": { "version": "3.3.0", @@ -2425,6 +2389,7 @@ "is-expression": { "version": "2.1.0", "from": "is-expression@^2.0.1", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-2.1.0.tgz", "dependencies": { "object-assign": { "version": "4.1.1", @@ -2436,19 +2401,23 @@ }, "doctypes": { "version": "1.1.0", - "from": "doctypes@^1.1.0" + "from": "doctypes@^1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz" }, "js-stringify": { "version": "1.0.2", - "from": "js-stringify@^1.0.1" + "from": "js-stringify@^1.0.1", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz" }, "pug-attrs": { - "version": "2.0.1", - "from": "pug-attrs@^2.0.1" + "version": "2.0.2", + "from": "pug-attrs@^2.0.2", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.2.tgz" }, "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.3.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" }, "void-elements": { "version": "2.0.1", @@ -2458,6 +2427,7 @@ "with": { "version": "5.1.1", "from": "with@^5.0.0", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", "dependencies": { "acorn": { "version": "3.3.0", @@ -2466,19 +2436,22 @@ }, "acorn-globals": { "version": "3.0.0", - "from": "acorn-globals@^3.0.0" + "from": "acorn-globals@^3.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.0.0.tgz" } } } } }, "pug-filters": { - "version": "1.2.4", - "from": "pug-filters@^1.2.4", + "version": "2.1.0", + "from": "pug-filters@^2.1.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-2.1.0.tgz", "dependencies": { "constantinople": { "version": "3.1.0", "from": "constantinople@^3.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.0.tgz", "dependencies": { "acorn": { "version": "3.3.0", @@ -2488,6 +2461,7 @@ "is-expression": { "version": "2.1.0", "from": "is-expression@^2.0.1", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-2.1.0.tgz", "dependencies": { "object-assign": { "version": "4.1.1", @@ -2498,16 +2472,19 @@ } }, "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.3.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" }, "pug-walk": { - "version": "1.0.0", - "from": "pug-walk@^1.0.0" + "version": "1.1.0", + "from": "pug-walk@^1.1.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz" }, "jstransformer": { "version": "1.0.0", "from": "jstransformer@1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", "dependencies": { "is-promise": { "version": "2.1.0", @@ -2544,8 +2521,7 @@ }, "source-map": { "version": "0.5.6", - "from": "source-map@~0.5.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + "from": "source-map@~0.5.1" }, "uglify-to-browserify": { "version": "1.0.2", @@ -2654,7 +2630,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" } } } @@ -2663,22 +2640,26 @@ } }, "pug-lexer": { - "version": "2.3.0", - "from": "pug-lexer@^2.2.0", + "version": "2.3.2", + "from": "pug-lexer@^2.3.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-2.3.2.tgz", "dependencies": { "character-parser": { "version": "2.2.0", "from": "character-parser@^2.1.1", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", "dependencies": { "is-regex": { "version": "1.0.3", - "from": "is-regex@^1.0.3" + "from": "is-regex@^1.0.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.3.tgz" } } }, "is-expression": { "version": "3.0.0", "from": "is-expression@^3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", "dependencies": { "acorn": { "version": "4.0.4", @@ -2692,73 +2673,81 @@ } }, "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.3.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" } } }, "pug-linker": { - "version": "1.0.1", - "from": "pug-linker@^1.0.1", + "version": "2.0.1", + "from": "pug-linker@^2.0.1", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-2.0.1.tgz", "dependencies": { "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.3.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" }, "pug-walk": { - "version": "1.0.0", - "from": "pug-walk@^1.0.0" + "version": "1.1.0", + "from": "pug-walk@^1.1.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz" } } }, "pug-load": { - "version": "2.0.3", - "from": "pug-load@^2.0.3", + "version": "2.0.4", + "from": "pug-load@^2.0.4", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.4.tgz", "dependencies": { "object-assign": { "version": "4.1.1", "from": "object-assign@^4.1.0" }, "pug-walk": { - "version": "1.0.0", - "from": "pug-walk@^1.0.0" + "version": "1.1.0", + "from": "pug-walk@^1.1.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.0.tgz" } } }, "pug-parser": { - "version": "2.0.1", - "from": "pug-parser@^2.0.1", + "version": "2.0.2", + "from": "pug-parser@^2.0.2", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-2.0.2.tgz", "dependencies": { "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.3.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" }, "token-stream": { "version": "0.0.1", - "from": "token-stream@0.0.1" + "from": "token-stream@0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz" } } }, "pug-runtime": { - "version": "2.0.2", - "from": "pug-runtime@^2.0.2" + "version": "2.0.3", + "from": "pug-runtime@^2.0.3", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.3.tgz" }, "pug-strip-comments": { - "version": "1.0.1", - "from": "pug-strip-comments@^1.0.1", + "version": "1.0.2", + "from": "pug-strip-comments@^1.0.2", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.2.tgz", "dependencies": { "pug-error": { - "version": "1.3.1", - "from": "pug-error@^1.0.0" + "version": "1.3.2", + "from": "pug-error@^1.3.2", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz" } } } } }, - "redback": { - "version": "0.4.0", - "from": "redback@0.4.0" - }, "redis": { "version": "0.10.1", "from": "redis@0.10.1" @@ -2766,6 +2755,7 @@ "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", @@ -2829,7 +2819,7 @@ "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@^0.2.0" + "from": "ansi-regex@^0.2.1" } } }, @@ -2839,7 +2829,7 @@ "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@^0.2.0" + "from": "ansi-regex@^0.2.1" } } }, @@ -2873,7 +2863,7 @@ }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@^0.5.0", + "from": "mkdirp@~0.5.1", "dependencies": { "minimist": { "version": "0.0.8", @@ -2884,12 +2874,10 @@ "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" } } }, @@ -2900,7 +2888,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", @@ -2909,7 +2896,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", @@ -2944,7 +2930,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", @@ -2954,8 +2939,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" } } } @@ -2980,16 +2964,15 @@ "jade": { "version": "0.26.3", "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "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" } } }, @@ -3009,7 +2992,7 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5" + "from": "mkdirp@~0.3.5" }, "glob": { "version": "3.2.3", @@ -3043,11 +3026,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", @@ -3108,7 +3093,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", @@ -3116,8 +3100,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", @@ -3144,12 +3127,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" } } }, @@ -3208,7 +3189,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", @@ -3226,8 +3206,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", @@ -3280,7 +3259,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", @@ -3310,8 +3288,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", @@ -3323,8 +3300,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", @@ -3336,8 +3312,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" } } } @@ -3371,8 +3346,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", @@ -3381,23 +3355,16 @@ "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" } } }, "tunnel-agent": { "version": "0.4.3", "from": "tunnel-agent@~0.4.1" - }, - "uuid": { - "version": "3.0.1", - "from": "uuid@^3.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz" } } }, @@ -3407,8 +3374,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", @@ -3454,22 +3420,32 @@ "version": "2.2.6", "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", - "from": "depd@^1.1.0" + "from": "depd@~1.1.0" }, "dottie": { "version": "1.1.1", @@ -3477,10 +3453,11 @@ }, "generic-pool": { "version": "2.4.2", - "from": "generic-pool@2.4.2" + "from": "generic-pool@2.4.2", + "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": { @@ -3489,8 +3466,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.13.0" }, "moment-timezone": { "version": "0.5.11", @@ -3503,7 +3479,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", @@ -3512,12 +3487,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", @@ -3525,20 +3498,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" } } } @@ -3564,17 +3534,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" } } }, @@ -3584,12 +3553,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" } } }, @@ -3600,7 +3569,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" } } }, @@ -3614,8 +3584,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" } } }, @@ -3623,19 +3592,21 @@ "version": "1.6.0", "from": "underscore@1.6.0" }, + "uuid": { + "version": "3.0.1", + "from": "uuid@^3.0.1" + }, "v8-profiler": { "version": "5.6.5", "from": "v8-profiler@^5.2.3", "dependencies": { "nan": { - "version": "2.5.0", - "from": "nan@^2.3.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.0.tgz" + "version": "2.5.1", + "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", @@ -3660,7 +3631,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", @@ -3673,7 +3643,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", @@ -3714,7 +3683,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", @@ -3734,18 +3702,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", @@ -3753,8 +3718,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" } } } @@ -3811,7 +3775,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", @@ -3820,7 +3783,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", @@ -3855,7 +3817,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", @@ -3865,8 +3826,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" } } } @@ -3890,8 +3850,7 @@ "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" } } }, @@ -3904,7 +3863,6 @@ "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", @@ -3922,8 +3880,7 @@ "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", @@ -3974,7 +3931,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", diff --git a/services/web/package.json b/services/web/package.json index 55dd7b0a18..100b55f75c 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -42,7 +42,6 @@ "mongojs": "0.18.2", "mongoose": "4.1.0", "multer": "^0.1.8", - "node-uuid": "1.4.1", "nodemailer": "2.1.0", "nodemailer-sendgrid-transport": "^0.2.0", "nodemailer-ses-transport": "^1.3.0", @@ -50,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", @@ -65,13 +63,17 @@ "v8-profiler": "^5.2.3", "xml2js": "0.2.0", "passport-saml": "^0.15.0", - "pug": "^2.0.0-beta6" + "pug": "^2.0.0-beta6", + "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", @@ -80,7 +82,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", @@ -88,9 +89,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/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index 2c6345d878..d9ca11231b 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -33,6 +33,11 @@ define [ response.success = true response.error = false + onSuccessHandler = scope[attrs.onSuccess] + if onSuccessHandler + onSuccessHandler(data, status, headers, config) + return + if data.redir? ga('send', 'event', formName, 'success') window.location = data.redir @@ -50,6 +55,12 @@ define [ scope[attrs.name].inflight = false response.success = false response.error = true + + onErrorHandler = scope[attrs.onError] + if onErrorHandler + onErrorHandler(data, status, headers, config) + return + if status == 403 # Forbidden response.message = text: "Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies." 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 9ceae66dbc..93775fd78f 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 722eab1aa5..e31b84f051 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -105,9 +105,8 @@ load = (EventEmitter) -> throw new Error("unknown op type") addComment: (op, metadata) -> - # TODO: Don't allow overlapping comments? @comments.push comment = { - id: @newId() + id: op.t or @newId() op: # Copy because we'll modify in place c: op.c p: op.p 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/img/about/chris.jpg b/services/web/public/img/about/chris.jpg new file mode 100644 index 0000000000..d317be783a Binary files /dev/null and b/services/web/public/img/about/chris.jpg differ diff --git a/services/web/public/img/about/geri.jpg b/services/web/public/img/about/geri.jpg deleted file mode 100644 index 0de2f9a20e..0000000000 Binary files a/services/web/public/img/about/geri.jpg and /dev/null differ 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/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index cf398e69da..bc1cb2e3b4 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -14,11 +14,20 @@ describe "CollaboratorsInviteController", -> @user = _id: 'id' @AnalyticsManger = recordEvent: sinon.stub() + @sendingUser = null @AuthenticationController = - getSessionUser: (req) => req.session.user + getSessionUser: (req) => + @sendingUser = req.session.user + return @sendingUser + + @RateLimiter = + addCount: sinon.stub + + @LimitationsManager = {} + @CollaboratorsInviteController = SandboxedModule.require modulePath, requires: "../Project/ProjectGetter": @ProjectGetter = {} - '../Subscription/LimitationsManager' : @LimitationsManager = {} + '../Subscription/LimitationsManager' : @LimitationsManager '../User/UserGetter': @UserGetter = {getUser: sinon.stub()} "./CollaboratorsHandler": @CollaboratorsHandler = {} "./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {} @@ -28,6 +37,7 @@ describe "CollaboratorsInviteController", -> "../Analytics/AnalyticsManager": @AnalyticsManger '../Authentication/AuthenticationController': @AuthenticationController 'settings-sharelatex': @settings = {} + "../../infrastructure/RateLimiter":@RateLimiter @res = new MockResponse() @req = new MockRequest() @@ -104,15 +114,10 @@ describe "CollaboratorsInviteController", -> describe 'when all goes well', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should produce json response', -> @res.json.callCount.should.equal 1 ({invite: @invite}).should.deep.equal(@res.json.firstCall.args[0]) @@ -122,8 +127,8 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @@ -136,22 +141,17 @@ describe "CollaboratorsInviteController", -> describe 'when the user is not allowed to add more collaborators', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, false) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should produce json response without an invite', -> @res.json.callCount.should.equal 1 ({invite: null}).should.deep.equal(@res.json.firstCall.args[0]) it 'should not have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 0 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -159,23 +159,18 @@ describe "CollaboratorsInviteController", -> describe 'when canAddXCollaborators produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @err = new Error('woops') @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, @err) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should call next with an error', -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true it 'should not have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 0 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal false + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal false it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -183,16 +178,11 @@ describe "CollaboratorsInviteController", -> describe 'when inviteToProject produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @err = new Error('woops') @CollaboratorsInviteHandler.inviteToProject = sinon.stub().callsArgWith(4, @err) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should call next with an error', -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true @@ -202,8 +192,8 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.calledWith(@project_id).should.equal true it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 1 @@ -212,22 +202,17 @@ describe "CollaboratorsInviteController", -> describe 'when _checkShouldInviteEmail disallows the invite', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, false) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, false) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should produce json response with no invite, and an error property', -> @res.json.callCount.should.equal 1 ({invite: null, error: 'cannot_invite_non_user'}).should.deep.equal(@res.json.firstCall.args[0]) it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -235,22 +220,17 @@ describe "CollaboratorsInviteController", -> describe 'when _checkShouldInviteEmail produces an error', -> beforeEach -> - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, new Error('woops')) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, new Error('woops')) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() - it 'should call next with an error', -> @next.callCount.should.equal 1 @next.calledWith(@err).should.equal true it 'should have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 1 - @_checkShouldInviteEmail.calledWith(@targetEmail).should.equal true + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 1 + @CollaboratorsInviteController._checkShouldInviteEmail.calledWith(@sendingUser, @targetEmail).should.equal true it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -260,14 +240,10 @@ describe "CollaboratorsInviteController", -> beforeEach -> @req.session.user = {_id: 'abc', email: 'me@example.com'} @req.body.email = 'me@example.com' - @_checkShouldInviteEmail = sinon.stub( - @CollaboratorsInviteController, '_checkShouldInviteEmail' - ).callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail = sinon.stub().callsArgWith(2, null, true) @LimitationsManager.canAddXCollaborators = sinon.stub().callsArgWith(2, null, true) @CollaboratorsInviteController.inviteToProject @req, @res, @next - afterEach -> - @_checkShouldInviteEmail.restore() it 'should reject action, return json response with error code', -> @res.json.callCount.should.equal 1 @@ -277,7 +253,7 @@ describe "CollaboratorsInviteController", -> @LimitationsManager.canAddXCollaborators.callCount.should.equal 0 it 'should not have called _checkShouldInviteEmail', -> - @_checkShouldInviteEmail.callCount.should.equal 0 + @CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal 0 it 'should not have called inviteToProject', -> @CollaboratorsInviteHandler.inviteToProject.callCount.should.equal 0 @@ -702,13 +678,14 @@ describe "CollaboratorsInviteController", -> beforeEach -> @email = 'user@example.com' - @call = (callback) => - @CollaboratorsInviteController._checkShouldInviteEmail @email, callback + describe 'when we should be restricting to existing accounts', -> beforeEach -> @settings.restrictInvitesToExistingAccounts = true + @call = (callback) => + @CollaboratorsInviteController._checkShouldInviteEmail {}, @email, callback describe 'when user account is present', -> @@ -753,18 +730,46 @@ describe "CollaboratorsInviteController", -> expect(shouldAllow).to.equal undefined done() - describe 'when we should not be restricting', -> + describe 'when we should not be restricting on only registered users but do rate limit', -> beforeEach -> @settings.restrictInvitesToExistingAccounts = false + @sendingUser = + _id:"32312313" + features: + collaborators:17.8 + @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @sendingUser) - it 'should callback with `true`', (done) -> - @call (err, shouldAllow) => - expect(err).to.equal null - expect(shouldAllow).to.equal true + it 'should callback with `true` when rate limit under', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.called.should.equal true + result.should.equal true done() - it 'should not have called getUser', (done) -> - @call (err, shouldAllow) => - @UserGetter.getUser.callCount.should.equal 0 + it 'should callback with `false` when rate limit hit', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, false) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.called.should.equal true + result.should.equal false done() + + it 'should call rate limiter with 10x the collaborators', (done) -> + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(178) + done() + + it 'should call rate limiter with 200 when collaborators is -1', (done) -> + @sendingUser.features.collaborators = -1 + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(200) + done() + + it 'should call rate limiter with 10 when user has no collaborators set', (done) -> + delete @sendingUser.features + @RateLimiter.addCount = sinon.stub().callsArgWith(1, null, true) + @CollaboratorsInviteController._checkShouldInviteEmail @sendingUser, @email, (err, result)=> + @RateLimiter.addCount.args[0][0].throttle.should.equal(10) + done() \ No newline at end of file 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) ->