From 30eb79dfdc1ac6375da57d76b0d9a4f9e97b400e Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 9 Jun 2015 15:36:16 +0100 Subject: [PATCH 01/57] Add link to DataJoy that shows randomly 50% of the time --- .../web/app/views/project/list/side-bar.jade | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade index a287ce5a23..4fee3eefd8 100644 --- a/services/web/app/views/project/list/side-bar.jade +++ b/services/web/app/views/project/list/side-bar.jade @@ -70,19 +70,33 @@ strong #{translate("create_your_first_project")} - if (showUserDetailsArea) - .row-spaced#userProfileInformation(ng-if="projects.length > 0", ng-cloak) - div(ng-controller="UserProfileController") - hr(ng-show="percentComplete < 100") - .text-centered.user-profile(ng-show="percentComplete < 100") - .progress - .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") - - p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} + - if (Math.random() < 0.5) + .row-spaced + hr + .card.card-thin + p.text-center.small + | Python or R user? - button#completeUserProfileInformation.btn.btn-info( - ng-hide="formVisable", - ng-click="openUserProfileModal()" - ) #{translate("complete")} + p.text-center.small + a(href="https://www.getdatajoy.com/", target="_blank").btn.btn-info.btn-small Try DataJoy + + p.text-center.small(style="font-size: 0.8em") + a(href="https://www.getdatajoy.com/", target="_blank") DataJoy + | is a new online Python and R editor from ShareLaTeX. + - else + .row-spaced#userProfileInformation(ng-if="projects.length > 0", ng-cloak) + div(ng-controller="UserProfileController") + hr(ng-show="percentComplete < 100") + .text-centered.user-profile(ng-show="percentComplete < 100") + .progress + .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") + + p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} + + button#completeUserProfileInformation.btn.btn-info( + ng-hide="formVisable", + ng-click="openUserProfileModal()" + ) #{translate("complete")} -if (settings.enableSubscriptions && !hasSubscription) .row-spaced(ng-if="projects.length > 0", ng-cloak).text-centered From 559e26145b70431239cdb2d5f7f96fc85151b327 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 16 Jun 2015 10:53:18 +0100 Subject: [PATCH 02/57] Run module unit tests with main tests --- services/web/Gruntfile.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 4efe88299c..d3fa598617 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -280,7 +280,7 @@ module.exports = (grunt) -> grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile'] - grunt.registerTask 'test:unit', 'Run the unit tests (use --grep= or --feature= for individual tests)', ['compile:server', 'compile:unit_tests', 'mochaTest:unit'] + grunt.registerTask 'test:unit', 'Run the unit tests (use --grep= or --feature= for individual tests)', ['compile:server', 'compile:modules:server', 'compile:unit_tests', 'compile:modules:unit_tests', 'mochaTest:unit'].concat(moduleUnitTestTasks) grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke'] grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks) From 706c1824f03e5ef6e3d22270aa0d3322638ccdb1 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 18 Jun 2015 15:43:42 +0100 Subject: [PATCH 03/57] changed package.json to use our fairy fork --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index 28082dcd03..c5a6078db5 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -17,7 +17,7 @@ "connect-redis": "1.4.5", "dateformat": "1.0.4-1.2.3", "express": "3.3.4", - "fairy": "0.0.2", + "fairy": "git+https://github.com/sharelatex/fairy.git#cluster", "http-proxy": "^1.8.1", "jade": "~1.3.1", "ldapjs": "^0.7.1", From 2ec925b45e20b6214e0fd64b2bf6d0b6e1315152 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 22 Jun 2015 22:28:17 +0100 Subject: [PATCH 04/57] fairy removed from web, makes http request to tpds worker now --- .../TpdsUpdateSender.coffee | 32 +++++++++++++++---- services/web/package.json | 1 - .../TpdsUpdateSenderTests.coffee | 21 ++++++------ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index f964720115..73426a0005 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -4,6 +4,7 @@ path = require('path') Project = require('../../models/Project').Project keys = require('../../infrastructure/Keys') metrics = require("../../infrastructure/Metrics") +request = require("request") buildPath = (user_id, project_name, filePath)-> projectPath = path.join(project_name, "/", filePath) @@ -11,9 +12,26 @@ buildPath = (user_id, project_name, filePath)-> fullPath = path.join("/user/", "#{user_id}", "/entity/",projectPath) return fullPath -queue = require('fairy').connect(settings.redis.fairy).queue(keys.queue.web_to_tpds_http_requests) -module.exports = + + + +module.exports = TpdsUpdateSender = + + _enqueue: (group, method, job, callback)-> + opts = + uri:"http://localhost:3030/enqueue/web_to_tpds_http_requests" + json : + group:group + method:method + job:job + method:"post" + + request opts, (err)-> + if err? + callback(err) + else + callback() _addEntity: (options, callback = (err)->)-> getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> @@ -27,9 +45,9 @@ module.exports = uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" title: "addFile" streamOrigin : options.streamOrigin - queue.enqueue options.project_id, "pipeStreamFrom", postOptions, -> + TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)-> logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store queued up for processing" - callback() + callback(err) addFile : (options, callback = (err)->)-> metrics.inc("tpds.add-file") @@ -64,7 +82,7 @@ module.exports = user_id : user_id endPath: endPath startPath: startPath - queue.enqueue options.project_id, "standardHttpRequest", moveOptions, callback + TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", moveOptions, callback deleteEntity : (options, callback = (err)->)-> metrics.inc("tpds.delete-entity") @@ -78,7 +96,7 @@ module.exports = uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}" title:"deleteEntity" sl_all_user_ids:JSON.stringify(allUserIds) - queue.enqueue options.project_id, "standardHttpRequest", deleteOptions, callback + TpdsUpdateSender._enqueue options.project_id, "standardHttpRequest", deleteOptions, callback pollDropboxForUser: (user_id, callback = (err) ->) -> metrics.inc("tpds.poll-dropbox") @@ -88,7 +106,7 @@ module.exports = uri:"#{settings.apis.thirdPartyDataStore.url}/user/poll" json: user_ids: [user_id] - queue.enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback + TpdsUpdateSender._enqueue "poll-dropbox:#{user_id}", "standardHttpRequest", options, callback getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)-> Project.findById project_id, "_id owner_ref readOnly_refs collaberator_refs", (err, project)-> diff --git a/services/web/package.json b/services/web/package.json index c5a6078db5..be11190a9b 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -17,7 +17,6 @@ "connect-redis": "1.4.5", "dateformat": "1.0.4-1.2.3", "express": "3.3.4", - "fairy": "git+https://github.com/sharelatex/fairy.git#cluster", "http-proxy": "^1.8.1", "jade": "~1.3.1", "ldapjs": "^0.7.1", diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index 2b7b691d8b..2957c8b48a 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee @@ -19,12 +19,11 @@ filestoreUrl = "filestore.sharelatex.com" describe 'TpdsUpdateSender', -> beforeEach -> - @requestQueuer = regist:(queue, meth, opts, callback)-> + @requestQueuer = (queue, meth, opts, callback)-> project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]} @Project = findById:sinon.stub().callsArgWith(2, null, project) @docstoreUrl = "docstore.sharelatex.env" @updateSender = SandboxedModule.require modulePath, requires: - 'fairy':{connect:=>{queue:=>@requestQueuer}} "settings-sharelatex": siteUrl:siteUrl httpAuthSiteUrl:httpAuthSiteUrl, @@ -34,17 +33,17 @@ describe 'TpdsUpdateSender', -> url: filestoreUrl docstore: pubUrl: @docstoreUrl - redis:fairy:{} + "logger-sharelatex":{log:->} '../../models/Project': Project:@Project 'request':->{pipe:->} describe 'sending updates', -> - it 'ques a post the file with user and file id', (done)-> + it 'queues a post the file with user and file id', (done)-> file_id = '4545345' path = '/some/path/here.jpg' - @requestQueuer.enqueue = (uid, method, job, callback)-> + @updateSender._enqueue = (uid, method, job, callback)-> uid.should.equal project_id job.method.should.equal "post" job.streamOrigin.should.equal "#{filestoreUrl}/project/#{project_id}/file/#{file_id}" @@ -59,7 +58,7 @@ describe 'TpdsUpdateSender', -> path = "/some/path/here.tex" lines = ["line1", "line2", "line3"] - @requestQueuer.enqueue = (uid, method, job, callback)=> + @updateSender._enqueue = (uid, method, job, callback)=> uid.should.equal project_id job.method.should.equal "post" expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}" @@ -71,7 +70,7 @@ describe 'TpdsUpdateSender', -> it 'deleting entity', (done)-> path = "/path/here/t.tex" - @requestQueuer.enqueue = (uid, method, job, callback)-> + @updateSender._enqueue = (uid, method, job, callback)-> uid.should.equal project_id job.method.should.equal "DELETE" expectedUrl = "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity/#{encodeURIComponent(project_name)}#{encodeURIComponent(path)}" @@ -83,7 +82,7 @@ describe 'TpdsUpdateSender', -> it 'moving entity', (done)-> startPath = "staring/here/file.tex" endPath = "ending/here/file.tex" - @requestQueuer.enqueue = (uid, method, job, callback)-> + @updateSender._enqueue = (uid, method, job, callback)-> uid.should.equal project_id job.method.should.equal "put" job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity" @@ -96,7 +95,7 @@ describe 'TpdsUpdateSender', -> it 'should be able to rename a project using the move entity func', (done)-> oldProjectName = "/oldProjectName/" newProjectName = "/newProjectName/" - @requestQueuer.enqueue = (uid, method, job, callback)-> + @updateSender._enqueue = (uid, method, job, callback)-> uid.should.equal project_id job.method.should.equal "put" job.uri.should.equal "#{thirdPartyDataStoreApiUrl}/user/#{user_id}/entity" @@ -107,9 +106,9 @@ describe 'TpdsUpdateSender', -> @updateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newProjectName} it "pollDropboxForUser", (done) -> - @requestQueuer.enqueue = sinon.stub().callsArg(3) + @updateSender._enqueue = sinon.stub().callsArg(3) @updateSender.pollDropboxForUser user_id, (error) => - @requestQueuer.enqueue + @updateSender._enqueue .calledWith( "poll-dropbox:#{user_id}", "standardHttpRequest", From b83fe4dcf9f27610034f71d09368c395bbe517be Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 23 Jun 2015 11:13:05 +0100 Subject: [PATCH 05/57] put tpdsworker url in from settings --- .../Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 3 +-- services/web/config/settings.defaults.coffee | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 73426a0005..7d41710fb1 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -20,13 +20,12 @@ module.exports = TpdsUpdateSender = _enqueue: (group, method, job, callback)-> opts = - uri:"http://localhost:3030/enqueue/web_to_tpds_http_requests" + uri:"#{settings.apis.tpdsworker.url}/enqueue/web_to_tpds_http_requests" json : group:group method:method job:job method:"post" - request opts, (err)-> if err? callback(err) diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index ba32e7c4d0..12b174e938 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -68,6 +68,8 @@ module.exports = thirdPartyDataStore: url : "http://localhost:3002" emptyProjectFlushDelayMiliseconds: 5 * seconds + tpdsworker: + url: "http://localhost:3030" tags: url :"http://localhost:3012" spelling: From 84bf0dd9a3a99e4521cfc719857d05031550200b Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 23 Jun 2015 11:19:23 +0100 Subject: [PATCH 06/57] added timeout and logging for tpdsworker queing via http --- .../Features/ThirdPartyDataStore/TpdsUpdateSender.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 7d41710fb1..4c96e29b59 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -26,10 +26,13 @@ module.exports = TpdsUpdateSender = method:method job:job method:"post" + timeout: (5 * 1000) request opts, (err)-> if err? + logger.err err:err, "error queuing something in the tpdsworker" callback(err) else + logger.log group:group, "successfully queued up job for tpdsworker" callback() _addEntity: (options, callback = (err)->)-> From 3de841dd719bd4451c7207cb6ce64334e6a4906e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 23 Jun 2015 13:50:42 +0100 Subject: [PATCH 07/57] added event loop monitor --- services/web/app/coffee/infrastructure/Server.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 2b2d80f0f2..b9d99b5e20 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -25,6 +25,8 @@ Modules = require "./Modules" metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger) +metrics.event_loop?.monitor(logger) + Settings.editorIsOpen ||= true if Settings.cacheStaticAssets From 893de9d8ac87cd11b8b93ade8c3ea0bcd6caa33e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 23 Jun 2015 13:54:05 +0100 Subject: [PATCH 08/57] updated metrics package version --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index be11190a9b..5f67a26090 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -22,7 +22,7 @@ "ldapjs": "^0.7.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "lynx": "0.1.1", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.1.0", "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", From a4f99c42240383532db7213e646036786198802e Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Tue, 23 Jun 2015 14:16:06 +0100 Subject: [PATCH 09/57] remove fairy from exception handler --- services/web/app.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/web/app.coffee b/services/web/app.coffee index efba66d4e4..a314885ccf 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -23,9 +23,6 @@ Server.app.use (error, req, res, next) -> res.end() if Settings.catchErrors - # fairy cleans then exits on an uncaughtError, but we don't want - # to exit so it doesn't need to do this. - require "fairy" process.removeAllListeners "uncaughtException" process.on "uncaughtException", (error) -> logger.error err: error, "uncaughtException" From 665bdcf538dd9fc1f7f9e7228ea7b638457438ad Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 12:04:41 +0100 Subject: [PATCH 10/57] v1 of express4 conversion --- .../AuthenticationController.coffee | 4 +- .../Subscription/SubscriptionRouter.coffee | 2 +- .../infrastructure/ExpressLocals.coffee | 2 +- .../app/coffee/infrastructure/Server.coffee | 85 +++++++++++-------- services/web/app/coffee/router.coffee | 13 ++- services/web/package.json | 24 +++++- 6 files changed, 81 insertions(+), 49 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 652abbe64d..5b934e4f25 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -7,6 +7,8 @@ logger = require("logger-sharelatex") querystring = require('querystring') Url = require("url") Settings = require "settings-sharelatex" +basicAuth = require('basic-auth-connect') + module.exports = AuthenticationController = login: (req, res, next = (error) ->) -> @@ -101,7 +103,7 @@ module.exports = AuthenticationController = logger.log url:req.url, "user trying to access endpoint not in global whitelist" return res.redirect "/login" - httpAuth: require('express').basicAuth (user, pass)-> + httpAuth: basicAuth (user, pass)-> isValid = Settings.httpAuthUsers[user] == pass if !isValid logger.err user:user, pass:pass, "invalid login details" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index a585a20e17..2db399152f 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -23,7 +23,7 @@ module.exports = app.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage app.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup app.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv - app.del '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup + app.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup app.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage app.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 4e67152b44..947070f532 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -95,7 +95,7 @@ module.exports = (app)-> next() app.use (req, res, next) -> - res.locals.csrfToken = req.session._csrf + res.locals.csrfToken = req.csrfToken() next() app.use (req, res, next) -> diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index b9d99b5e20..6ae27dc0eb 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -7,14 +7,22 @@ crawlerLogger = require('./CrawlerLogger') expressLocals = require('./ExpressLocals') Router = require('../router') metrics.inc("startup") - redis = require("redis-sharelatex") rclient = redis.createClient(Settings.redis.web) -RedisStore = require('connect-redis')(express) +session = require("express-session") +RedisStore = require('connect-redis')(session) +bodyParser = require('body-parser') +multer = require('multer') +methodOverride = require('method-override') +csrf = require('csurf') +csrfProtection = csrf() +cookieParser = require('cookie-parser') + sessionStore = new RedisStore(client:rclient) -cookieParser = express.cookieParser(Settings.security.sessionSecret) +Mongoose = require("./Mongoose") + oneDayInMilliseconds = 86400000 ReferalConnect = require('../Features/Referal/ReferalConnect') RedirectManager = require("./RedirectManager") @@ -36,49 +44,52 @@ else app = express() -csrf = express.csrf() ignoreCsrfRoutes = [] app.ignoreCsrf = (method, route) -> ignoreCsrfRoutes.push new express.Route(method, route) -app.configure () -> - if Settings.behindProxy - app.enable('trust proxy') - app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) - app.set 'views', __dirname + '/../../views' - app.set 'view engine', 'jade' - Modules.loadViewIncludes app - app.use express.bodyParser(uploadDir: Settings.path.uploadFolder) - app.use translations.expressMiddlewear - app.use translations.setLangBasedOnDomainMiddlewear - app.use cookieParser - app.use express.session - proxy: Settings.behindProxy - cookie: - domain: Settings.cookieDomain - maxAge: Settings.cookieSessionLength - secure: Settings.secureCookie - store: sessionStore - key: Settings.cookieName - - # Measure expiry from last request, not last login - app.use (req, res, next) -> - req.session.touch() - next() - - app.use (req, res, next) -> - for route in ignoreCsrfRoutes - if route.method == req.method?.toLowerCase() and route.match(req.path) - return next() - csrf(req, res, next) +if Settings.behindProxy + app.enable('trust proxy') +app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) +app.set 'views', __dirname + '/../../views' +app.set 'view engine', 'jade' +Modules.loadViewIncludes app +app.use cookieParser(Settings.security.sessionSecret) +app.use session + resave: false + secret:Settings.security.sessionSecret + proxy: Settings.behindProxy + cookie: + domain: Settings.cookieDomain + maxAge: Settings.cookieSessionLength + secure: Settings.secureCookie + store: sessionStore + key: Settings.cookieName - app.use ReferalConnect.use - app.use express.methodOverride() +app.use bodyParser.urlencoded({ extended: true }) +app.use bodyParser.json() +app.use multer(dest: Settings.path.uploadFolder) +app.use translations.expressMiddlewear +app.use translations.setLangBasedOnDomainMiddlewear + +# Measure expiry from last request, not last login +app.use (req, res, next) -> + req.session.touch() + next() + +app.use (req, res, next) -> + for route in ignoreCsrfRoutes + if route.method == req.method?.toLowerCase() and route.match(req.path) + return next() + csrfProtection(req, res, next) + +app.use ReferalConnect.use +app.use methodOverride() expressLocals(app) -app.configure 'production', -> +if app.get('env') == 'production' logger.info "Production Enviroment" app.enable('view cache') diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index ae4121a456..529f8a51d3 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -44,7 +44,6 @@ module.exports = class Router if !Settings.allowPublicAccess app.all '*', AuthenticationController.requireGlobalLogin - app.use(app.router) app.get '/login', UserPagesController.loginPage AuthenticationController.addEndpointToLoginWhitelist '/login' @@ -77,8 +76,8 @@ module.exports = class Router app.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings app.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword - app.del '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe - app.del '/user', AuthenticationController.requireLogin(), UserController.deleteUser + app.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe + app.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser app.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken app.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo @@ -107,11 +106,11 @@ module.exports = class Router req.params = params next() ), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi - app.del "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles + app.delete "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles app.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync app.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync - app.del '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject + app.delete '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject app.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject app.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject @@ -138,12 +137,12 @@ module.exports = class Router app.ignoreCsrf('post', '/project/:Project_id/doc/:doc_id') app.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate - app.del '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate + app.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate app.ignoreCsrf('post', '/user/:user_id/update/*') app.ignoreCsrf('delete', '/user/:user_id/update/*') app.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents - app.del '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents + app.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents app.ignoreCsrf('post', '/project/:project_id/contents/*') app.ignoreCsrf('delete', '/project/:project_id/contents/*') diff --git a/services/web/package.json b/services/web/package.json index 5f67a26090..5cb505a93e 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -12,21 +12,41 @@ "dependencies": { "archiver": "0.9.0", "async": "0.6.2", +<<<<<<< HEAD +======= + "base64-stream": "^0.1.2", + "basic-auth-connect": "^1.0.0", +>>>>>>> 90b15d3... v1 of express4 conversion "bcrypt": "0.8.3", + "body-parser": "^1.13.1", "bufferedstream": "1.6.0", - "connect-redis": "1.4.5", + "connect-redis": "2.3.0", + "cookie-parser": "1.3.5", + "csurf": "^1.8.3", "dateformat": "1.0.4-1.2.3", - "express": "3.3.4", + "express": "4.13.0", + "express-session": "1.11.3", "http-proxy": "^1.8.1", "jade": "~1.3.1", "ldapjs": "^0.7.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "lynx": "0.1.1", +<<<<<<< HEAD +======= + "marked": "^0.3.3", + "method-override": "^2.3.3", +>>>>>>> 90b15d3... v1 of express4 conversion "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.1.0", "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", "mongoose": "3.8.28", +<<<<<<< HEAD +======= + "mongoose-auto-increment": "3.0.2", + "multer": "^0.1.8", + "mysql": "^2.7.0", +>>>>>>> 90b15d3... v1 of express4 conversion "node-uuid": "1.4.1", "nodemailer": "0.6.1", "optimist": "0.6.1", From 1cc0cbe8fc4f89709417305de9d2ba9bac9492be Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 14:38:32 +0100 Subject: [PATCH 11/57] split site into 2 routers, webRouter and apiRouter web router has things like sessions etc added onto it. Api router is minimal, doesn't include things like csrf --- .../Features/Analytics/AnalyticsRouter.coffee | 5 + .../Collaborators/CollaboratorsRouter.coffee | 10 +- .../Features/Editor/EditorRouter.coffee | 21 +- .../PasswordReset/PasswordResetRouter.coffee | 10 +- .../RealTimeProxy/RealTimeProxyRouter.coffee | 4 +- .../StaticPages/StaticPagesRouter.coffee | 24 +-- .../Subscription/SubscriptionRouter.coffee | 45 ++-- .../Features/Uploads/UploadsRouter.coffee | 6 +- .../infrastructure/ExpressLocals.coffee | 6 +- .../app/coffee/infrastructure/Modules.coffee | 4 +- .../app/coffee/infrastructure/Server.coffee | 65 +++--- services/web/app/coffee/router.coffee | 201 +++++++++--------- 12 files changed, 206 insertions(+), 195 deletions(-) create mode 100644 services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee new file mode 100644 index 0000000000..a9e0a0028b --- /dev/null +++ b/services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee @@ -0,0 +1,5 @@ +AnalyticsController = require('./AnalyticsController') + +module.exports = + apply: (webRouter, apiRouter) -> + webRouter.post '/event/:event', AnalyticsController.recordEvent diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index d7df1f299b..b327c50e9d 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -3,9 +3,9 @@ SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require('../Authentication/AuthenticationController') module.exports = - apply: (app) -> - app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject - app.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators + apply: (webRouter, apiRouter) -> + webRouter.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject + apiRouter.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators - app.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject - app.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject + webRouter.post '/project/:Project_id/users', SecurityManager.requestIsOwner, CollaboratorsController.addUserToProject + webRouter.delete '/project/:Project_id/users/:user_id', SecurityManager.requestIsOwner, CollaboratorsController.removeUserFromProject diff --git a/services/web/app/coffee/Features/Editor/EditorRouter.coffee b/services/web/app/coffee/Features/Editor/EditorRouter.coffee index 1902506948..0576a4adce 100644 --- a/services/web/app/coffee/Features/Editor/EditorRouter.coffee +++ b/services/web/app/coffee/Features/Editor/EditorRouter.coffee @@ -3,21 +3,20 @@ SecurityManager = require('../../managers/SecurityManager') AuthenticationController = require "../Authentication/AuthenticationController" module.exports = - apply: (app) -> - app.post '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc - app.post '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder + apply: (webRouter, apiRouter) -> + webRouter.post '/project/:Project_id/doc', SecurityManager.requestCanModifyProject, EditorHttpController.addDoc + webRouter.post '/project/:Project_id/folder', SecurityManager.requestCanModifyProject, EditorHttpController.addFolder - app.post '/project/:Project_id/:entity_type/:entity_id/rename', SecurityManager.requestCanModifyProject, EditorHttpController.renameEntity - app.post '/project/:Project_id/:entity_type/:entity_id/move', SecurityManager.requestCanModifyProject, EditorHttpController.moveEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', SecurityManager.requestCanModifyProject, EditorHttpController.renameEntity + webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', SecurityManager.requestCanModifyProject, EditorHttpController.moveEntity - app.delete '/project/:Project_id/file/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFile - app.delete '/project/:Project_id/doc/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteDoc - app.delete '/project/:Project_id/folder/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFolder + webRouter.delete '/project/:Project_id/file/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFile + webRouter.delete '/project/:Project_id/doc/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteDoc + webRouter.delete '/project/:Project_id/folder/:entity_id', SecurityManager.requestCanModifyProject, EditorHttpController.deleteFolder - app.post '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, EditorHttpController.restoreDoc + webRouter.post '/project/:Project_id/doc/:doc_id/restore', SecurityManager.requestCanModifyProject, EditorHttpController.restoreDoc # Called by the real-time API to load up the current project state. # This is a post request because it's more than just a getting of data. We take actions # whenever a user joins a project, like updating the deleted status. - app.post '/project/:Project_id/join', AuthenticationController.httpAuth, EditorHttpController.joinProject - app.ignoreCsrf('post', '/project/:Project_id/join') \ No newline at end of file + apiRouter.post '/project/:Project_id/join', AuthenticationController.httpAuth, EditorHttpController.joinProject diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee index 057304d458..e049d43075 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetRouter.coffee @@ -2,13 +2,13 @@ PasswordResetController = require("./PasswordResetController") AuthenticationController = require('../Authentication/AuthenticationController') module.exports = - apply: (app) -> + apply: (webRouter, apiRouter) -> - app.get '/user/password/reset', PasswordResetController.renderRequestResetForm - app.post '/user/password/reset', PasswordResetController.requestReset + webRouter.get '/user/password/reset', PasswordResetController.renderRequestResetForm + webRouter.post '/user/password/reset', PasswordResetController.requestReset AuthenticationController.addEndpointToLoginWhitelist '/user/password/reset' - app.get '/user/password/set', PasswordResetController.renderSetPasswordForm - app.post '/user/password/set', PasswordResetController.setNewUserPassword + webRouter.get '/user/password/set', PasswordResetController.renderSetPasswordForm + webRouter.post '/user/password/set', PasswordResetController.setNewUserPassword AuthenticationController.addEndpointToLoginWhitelist '/user/password/set' diff --git a/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee b/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee index 4fbf06c091..0672520f31 100644 --- a/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee +++ b/services/web/app/coffee/Features/RealTimeProxy/RealTimeProxyRouter.coffee @@ -10,8 +10,8 @@ wsProxy = httpProxy.createProxyServer({ }) module.exports = - apply: (app) -> - app.all /\/socket\.io\/.*/, (req, res, next) -> + apply: (webRouter, apiRouter) -> + webRouter.all /\/socket\.io\/.*/, (req, res, next) -> proxy.web req, res, next setTimeout () -> diff --git a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee b/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee index ab68fd7469..f1079cc5b7 100644 --- a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee +++ b/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee @@ -3,18 +3,18 @@ UniversityController = require("./UniversityController") module.exports = - apply: (app) -> - app.get '/', HomeController.index - app.get '/home', HomeController.home + apply: (webRouter, apiRouter) -> + webRouter.get '/', HomeController.index + webRouter.get '/home', HomeController.home - app.get '/tos', HomeController.externalPage("tos", "Terms of Service") - app.get '/about', HomeController.externalPage("about", "About Us") - app.get '/security', HomeController.externalPage("security", "Security") - app.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy") - app.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance") - app.get '/style', HomeController.externalPage("style_guide", "Style Guide") + webRouter.get '/tos', HomeController.externalPage("tos", "Terms of Service") + webRouter.get '/about', HomeController.externalPage("about", "About Us") + webRouter.get '/security', HomeController.externalPage("security", "Security") + webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy") + webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance") + webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide") - app.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX") + webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX") - app.get '/university', UniversityController.getIndexPage - app.get '/university/*', UniversityController.getPage \ No newline at end of file + webRouter.get '/university', UniversityController.getIndexPage + webRouter.get '/university/*', UniversityController.getPage \ No newline at end of file diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index 2db399152f..76dded3f4d 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -4,44 +4,43 @@ SubscriptionGroupController = require './SubscriptionGroupController' Settings = require "settings-sharelatex" module.exports = - apply: (app) -> + apply: (webRouter, apiRouter) -> return unless Settings.enableSubscriptions - app.get '/user/subscription/plans', SubscriptionController.plansPage + webRouter.get '/user/subscription/plans', SubscriptionController.plansPage - app.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage + webRouter.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage - app.get '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage + webRouter.get '/user/subscription/custom_account', AuthenticationController.requireLogin(), SubscriptionController.userCustomSubscriptionPage - app.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage - app.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage + webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage + webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage - app.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription + webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription - app.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage - app.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup - app.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv - app.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup + webRouter.get '/subscription/group', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSubscriptionGroupAdminPage + webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup + webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv + webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup - app.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage - app.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup - app.get '/user/subscription/:subscription_id/group/complete-join', AuthenticationController.requireLogin(), SubscriptionGroupController.completeJoin - app.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage + webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage + webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup + webRouter.get '/user/subscription/:subscription_id/group/complete-join', AuthenticationController.requireLogin(), SubscriptionGroupController.completeJoin + webRouter.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage #recurly callback - app.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback - app.ignoreCsrf("post", '/user/subscription/callback') + apiRouter.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback #user changes their account state - app.post '/user/subscription/create', AuthenticationController.requireLogin(), SubscriptionController.createSubscription - app.post '/user/subscription/update', AuthenticationController.requireLogin(), SubscriptionController.updateSubscription - app.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription - app.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription + webRouter.post '/user/subscription/create', AuthenticationController.requireLogin(), SubscriptionController.createSubscription + webRouter.post '/user/subscription/update', AuthenticationController.requireLogin(), SubscriptionController.updateSubscription + webRouter.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription + webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription - app.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage - app.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan + webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage + webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index f77cb4bfc5..bf4b9f3ea4 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -3,11 +3,11 @@ AuthenticationController = require('../Authentication/AuthenticationController') ProjectUploadController = require "./ProjectUploadController" module.exports = - apply: (app) -> - app.post '/project/new/upload', + apply: (webRouter, apiRouter) -> + webRouter.post '/project/new/upload', AuthenticationController.requireLogin(), ProjectUploadController.uploadProject - app.post '/Project/:Project_id/upload', + webRouter.post '/Project/:Project_id/upload', SecurityManager.requestCanModifyProject, ProjectUploadController.uploadFile diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 947070f532..e11d22db94 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -39,7 +39,7 @@ for path in [ logger.log filePath:filePath, "file does not exist for fingerprints" -module.exports = (app)-> +module.exports = (app, webRouter, apiRouter)-> app.use (req, res, next)-> res.locals.session = req.session next() @@ -94,8 +94,8 @@ module.exports = (app)-> return "" next() - app.use (req, res, next) -> - res.locals.csrfToken = req.csrfToken() + webRouter.use (req, res, next) -> + res.locals.csrfToken = req?.csrfToken() next() app.use (req, res, next) -> diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee index 0aedf6bbf0..720819e503 100644 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ b/services/web/app/coffee/infrastructure/Modules.coffee @@ -13,9 +13,9 @@ module.exports = Modules = loadedModule.name = moduleName @modules.push loadedModule - applyRouter: (app) -> + applyRouter: (webRouter, apiRouter) -> for module in @modules - module.router?.apply(app) + module.router?.apply(webRouter, apiRouter) viewIncludes: {} loadViewIncludes: (app) -> diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 6ae27dc0eb..b7a441bfce 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -44,19 +44,31 @@ else app = express() -ignoreCsrfRoutes = [] -app.ignoreCsrf = (method, route) -> - ignoreCsrfRoutes.push new express.Route(method, route) - +webRouter = express.Router() +apiRouter = express.Router() if Settings.behindProxy app.enable('trust proxy') -app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) + +webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) app.set 'views', __dirname + '/../../views' app.set 'view engine', 'jade' Modules.loadViewIncludes app -app.use cookieParser(Settings.security.sessionSecret) -app.use session + + + +app.use bodyParser.urlencoded({ extended: true }) +app.use bodyParser.json() +app.use multer(dest: Settings.path.uploadFolder) +app.use methodOverride() + +app.use metrics.http.monitor(logger) +app.use RedirectManager +app.use OldAssetProxy + + +webRouter.use cookieParser(Settings.security.sessionSecret) +webRouter.use session resave: false secret:Settings.security.sessionSecret proxy: Settings.behindProxy @@ -66,36 +78,23 @@ app.use session secure: Settings.secureCookie store: sessionStore key: Settings.cookieName - -app.use bodyParser.urlencoded({ extended: true }) -app.use bodyParser.json() -app.use multer(dest: Settings.path.uploadFolder) -app.use translations.expressMiddlewear -app.use translations.setLangBasedOnDomainMiddlewear +webRouter.use csrfProtection +webRouter.use translations.expressMiddlewear +webRouter.use translations.setLangBasedOnDomainMiddlewear # Measure expiry from last request, not last login -app.use (req, res, next) -> +webRouter.use (req, res, next) -> req.session.touch() next() -app.use (req, res, next) -> - for route in ignoreCsrfRoutes - if route.method == req.method?.toLowerCase() and route.match(req.path) - return next() - csrfProtection(req, res, next) - -app.use ReferalConnect.use -app.use methodOverride() - -expressLocals(app) +webRouter.use ReferalConnect.use +expressLocals(app, webRouter, apiRouter) if app.get('env') == 'production' logger.info "Production Enviroment" app.enable('view cache') -app.use metrics.http.monitor(logger) -app.use RedirectManager -app.use OldAssetProxy + app.use (req, res, next)-> metrics.inc "http-request" @@ -109,12 +108,11 @@ app.use (req, res, next) -> else next() -app.get "/status", (req, res)-> +apiRouter.get "/status", (req, res)-> res.send("web sharelatex is alive") - req.session.destroy() profiler = require "v8-profiler" -app.get "/profile", (req, res) -> +apiRouter.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") profiler.startProfiling("test") setTimeout () -> @@ -125,7 +123,12 @@ app.get "/profile", (req, res) -> logger.info ("creating HTTP server").yellow server = require('http').createServer(app) -router = new Router(app) +# process api routes first, if nothing matched fall though and use +# web middlewear + routes +app.use(apiRouter) +app.use(webRouter) + +router = new Router(webRouter, apiRouter) module.exports = app: app diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 529f8a51d3..d9ff55a0a3 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -40,65 +40,67 @@ logger = require("logger-sharelatex") _ = require("underscore") module.exports = class Router - constructor: (app)-> + constructor: (webRouter, apiRouter)-> if !Settings.allowPublicAccess - app.all '*', AuthenticationController.requireGlobalLogin + webRouter.all '*', AuthenticationController.requireGlobalLogin - app.get '/login', UserPagesController.loginPage + webRouter.get '/login', UserPagesController.loginPage AuthenticationController.addEndpointToLoginWhitelist '/login' - app.post '/login', AuthenticationController.login - app.get '/logout', UserController.logout - app.get '/restricted', SecurityManager.restricted + webRouter.post '/login', AuthenticationController.login + webRouter.get '/logout', UserController.logout + webRouter.get '/restricted', SecurityManager.restricted # Left as a placeholder for implementing a public register page - app.get '/register', UserPagesController.registerPage + webRouter.get '/register', UserPagesController.registerPage AuthenticationController.addEndpointToLoginWhitelist '/register' - EditorRouter.apply(app) - CollaboratorsRouter.apply(app) - SubscriptionRouter.apply(app) - UploadsRouter.apply(app) - PasswordResetRouter.apply(app) - StaticPagesRouter.apply(app) - RealTimeProxyRouter.apply(app) - - Modules.applyRouter(app) - app.get '/blog', BlogController.getIndexPage - app.get '/blog/*', BlogController.getPage + EditorRouter.apply(webRouter, apiRouter) + CollaboratorsRouter.apply(webRouter, apiRouter) + SubscriptionRouter.apply(webRouter, apiRouter) + UploadsRouter.apply(webRouter, apiRouter) + PasswordResetRouter.apply(webRouter, apiRouter) + StaticPagesRouter.apply(webRouter, apiRouter) + RealTimeProxyRouter.apply(webRouter, apiRouter) + AnalyticsRouter.apply(webRouter, apiRouter) + + Modules.applyRouter(webRouter, apiRouter) + if Settings.enableSubscriptions - app.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus + webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus + + webRouter.get '/blog', BlogController.getIndexPage + webRouter.get '/blog/*', BlogController.getPage + + webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage + webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings + webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword - app.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage - app.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings - app.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword + webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe + webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser - app.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe - app.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser + webRouter.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken + webRouter.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo + apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo - app.get '/user/auth_token', AuthenticationController.requireLogin(), AuthenticationController.getAuthToken - app.get '/user/personal_info', AuthenticationController.requireLogin(allow_auth_token: true), UserInfoController.getLoggedInUsersPersonalInfo - app.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo + webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage + webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject - app.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage - app.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject - - app.get '/Project/:Project_id', RateLimiterMiddlewear.rateLimit({ + webRouter.get '/Project/:Project_id', RateLimiterMiddlewear.rateLimit({ endpointName: "open-project" params: ["Project_id"] maxRequests: 10 timeInterval: 60 }), SecurityManager.requestCanAccessProject, ProjectController.loadEditor - app.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile + webRouter.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile + webRouter.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings - app.post '/project/:Project_id/settings', SecurityManager.requestCanModifyProject, ProjectController.updateProjectSettings - - app.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile - app.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf - app.get /^\/project\/([^\/]*)\/output\/(.*)$/, + webRouter.post '/project/:Project_id/compile', SecurityManager.requestCanAccessProject, CompileController.compile + webRouter.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf + webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = "Project_id": req.params[0] @@ -106,78 +108,82 @@ module.exports = class Router req.params = params next() ), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi - app.delete "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles - app.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync - app.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync + webRouter.delete "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles + webRouter.get "/project/:Project_id/sync/code", SecurityManager.requestCanAccessProject, CompileController.proxySync + webRouter.get "/project/:Project_id/sync/pdf", SecurityManager.requestCanAccessProject, CompileController.proxySync - app.delete '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject - app.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject - app.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject + webRouter.delete '/Project/:Project_id', SecurityManager.requestIsOwner, ProjectController.deleteProject + webRouter.post '/Project/:Project_id/restore', SecurityManager.requestIsOwner, ProjectController.restoreProject + webRouter.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, ProjectController.cloneProject - app.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject + webRouter.post '/project/:Project_id/rename', SecurityManager.requestIsOwner, ProjectController.renameProject - app.get "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - app.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi + webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi - app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject - app.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects + webRouter.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject + webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects - app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags - app.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate + webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags + webRouter.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate - app.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails + # Deprecated in favour of /internal/project/:project_id but still used by versioning + apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails - app.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject - app.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf - - - app.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument - app.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument - app.ignoreCsrf('post', '/project/:Project_id/doc/:doc_id') - - app.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate - app.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate - app.ignoreCsrf('post', '/user/:user_id/update/*') - app.ignoreCsrf('delete', '/user/:user_id/update/*') + # New 'stable' /internal API end points + apiRouter.get '/internal/project/:project_id', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails + apiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject - app.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents - app.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents - app.ignoreCsrf('post', '/project/:project_id/contents/*') - app.ignoreCsrf('delete', '/project/:project_id/contents/*') + webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "file": req.params[1] + req.params = params + next() + ), AuthenticationController.httpAuth, CompileController.getFileFromClsi - app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + apiRouter.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument + apiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument - app.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages - app.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage + apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate + apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate - app.get /learn(\/.*)?/, WikiController.getPage + apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents + apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents + + webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + + webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages + webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage + + webRouter.get /learn(\/.*)?/, WikiController.getPage #Admin Stuff - app.get '/admin', SecurityManager.requestIsAdmin, AdminController.index - app.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser - app.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register - app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor - app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers - app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription - app.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds - app.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser - app.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage - app.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages + webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index + webRouter.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser + webRouter.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register + webRouter.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor + webRouter.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers + webRouter.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription + webRouter.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds + webRouter.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser + webRouter.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage + webRouter.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages - app.get '/perfTest', (req,res)-> + apiRouter.get '/perfTest', (req,res)-> res.send("hello") - req.session.destroy() - app.get '/status', (req,res)-> + apiRouter.get '/status', (req,res)-> res.send("websharelatex is up") - req.session.destroy() + - app.get '/health_check', HealthCheckController.check - app.get '/health_check/redis', HealthCheckController.checkRedis + webRouter.get '/health_check', HealthCheckController.check + webRouter.get '/health_check/redis', HealthCheckController.checkRedis - app.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) -> + apiRouter.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) -> sendRes = _.once (statusCode, message)-> res.writeHead statusCode res.end message @@ -186,27 +192,26 @@ module.exports = class Router setTimeout (() -> sendRes 500, "Compiler timed out" ), 10000 - req.session.destroy() - app.get "/ip", (req, res, next) -> + apiRouter.get "/ip", (req, res, next) -> res.send({ ip: req.ip ips: req.ips headers: req.headers }) - app.get '/oops-express', (req, res, next) -> next(new Error("Test error")) - app.get '/oops-internal', (req, res, next) -> throw new Error("Test error") - app.get '/oops-mongo', (req, res, next) -> + apiRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) + apiRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") + apiRouter.get '/oops-mongo', (req, res, next) -> require("./models/Project").Project.findOne {}, () -> throw new Error("Test error") - app.get '/opps-small', (req, res, next)-> + apiRouter.get '/opps-small', (req, res, next)-> logger.err "test error occured" res.send() - app.post '/error/client', (req, res, next) -> + webRouter.post '/error/client', (req, res, next) -> logger.error err: req.body.error, meta: req.body.meta, "client side error" res.send(204) - app.get '*', ErrorController.notFound + webRouter.get '*', ErrorController.notFound From 15a57f5dc404a1c382651e3528a1d5aeab2d95a6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 14:39:37 +0100 Subject: [PATCH 12/57] removed req.session.destorys from endpoints now on the api router which are not needed --- .../Features/Collaborators/CollaboratorsController.coffee | 1 - .../coffee/Features/Documents/DocumentController.coffee | 2 -- .../Features/ThirdPartyDataStore/TpdsController.coffee | 4 ---- .../web/app/coffee/Features/User/UserController.coffee | 2 +- .../web/app/coffee/Features/User/UserInfoController.coffee | 3 +-- .../coffee/Project/ProjectApiControllerTests.coffee | 7 ------- .../coffee/ThirdPartyDataStore/TpdsControllerTests.coffee | 5 ----- 7 files changed, 2 insertions(+), 22 deletions(-) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 13cc60287f..e956b7f6bf 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -4,7 +4,6 @@ EditorController = require "../Editor/EditorController" module.exports = CollaboratorsController = getCollaborators: (req, res, next = (error) ->) -> - req.session.destroy() ProjectGetter.getProject req.params.Project_id, { owner_ref: true, collaberator_refs: true, readOnly_refs: true}, (error, project) -> return next(error) if error? ProjectGetter.populateProjectWithUsers project, (error, project) -> diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 9cf8525bac..1170da2ef4 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -15,7 +15,6 @@ module.exports = res.send JSON.stringify { lines: lines } - req.session.destroy() setDocument: (req, res, next = (error) ->) -> project_id = req.params.Project_id @@ -28,7 +27,6 @@ module.exports = return next(error) logger.log doc_id:doc_id, project_id:project_id, "finished receiving set document request from api (docupdater)" res.send 200 - req.session.destroy() diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee index ef22e1764c..3dbb30f75c 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee @@ -21,7 +21,6 @@ module.exports = else logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed" res.send 200 - req.session.destroy() deleteUpdate: (req, res)-> @@ -36,7 +35,6 @@ module.exports = else logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed" res.send 200 - req.session.destroy() # updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we # can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy @@ -50,7 +48,6 @@ module.exports = UpdateMerger.mergeUpdate project_id, path, req, source, (error) -> return next(error) if error? res.send(200) - req.session.destroy() deleteProjectContents: (req, res, next = (error) ->) -> {project_id} = req.params @@ -60,7 +57,6 @@ module.exports = UpdateMerger.deleteUpdate project_id, path, source, (error) -> return next(error) if error? res.send(200) - req.session.destroy() parseParams: parseParams = (req)-> path = req.params[0] diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index a374939545..643404a14d 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -20,7 +20,7 @@ module.exports = user_id = req.session.user._id UserDeleter.deleteUser user_id, (err)-> if !err? - req.session.destroy() + req.session?.destroy() res.send(200) unsubscribe: (req, res)-> diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index 65cbd34bad..ac7556bc90 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -9,7 +9,7 @@ module.exports = UserController = # this is funcky as hell, we don't use the current session to get the user # we use the auth token, actually destroying session from the chat api request if req.query?.auth_token? - req.session.destroy() + req.session?.destroy() logger.log user: req.user, "reciving request for getting logged in users personal info" return next(new Error("User is not logged in")) if !req.user? UserGetter.getUser req.user._id, { @@ -26,7 +26,6 @@ module.exports = UserController = return next(error) if error? return res.send(404) if !user? UserController.sendFormattedPersonalInfo(user, res, next) - req.session.destroy() sendFormattedPersonalInfo: (user, res, next = (error) ->) -> UserController._formatPersonalInfo user, (error, info) -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee index fbd50bb3ce..322d61750e 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee @@ -40,10 +40,3 @@ describe 'Project api controller', -> resCode.should.equal 500 done() @controller.getProjectDetails @req, @res - - it "should destroy the session", (done)-> - @ProjectDetailsHandler.getDetails.callsArgWith(1, null, @projDetails) - @res.json = (data)=> - @req.session.destroy.called.should.equal true - done() - @controller.getProjectDetails @req, @res \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee index e43737a478..769582a95a 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee @@ -98,8 +98,6 @@ describe 'TpdsController', -> it "should return a success", -> @res.send.calledWith(200).should.equal true - it "should clear the session", -> - @req.session.destroy.called.should.equal true describe 'deleteProjectContents', -> beforeEach -> @@ -125,6 +123,3 @@ describe 'TpdsController', -> it "should return a success", -> @res.send.calledWith(200).should.equal true - it "should clear the session", -> - @req.session.destroy.called.should.equal true - From 941d4072317aa7c7cc42634c79c499c508afcf11 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 15:32:01 +0100 Subject: [PATCH 13/57] added saveUninitialized option to session which is now required --- services/web/app/coffee/infrastructure/Server.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index b7a441bfce..98cd7992dd 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -70,6 +70,7 @@ app.use OldAssetProxy webRouter.use cookieParser(Settings.security.sessionSecret) webRouter.use session resave: false + saveUninitialized:false secret:Settings.security.sessionSecret proxy: Settings.behindProxy cookie: From 3ab57f68309ec332d088e150f3d11b6c495f9347 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 15:36:39 +0100 Subject: [PATCH 14/57] put express locals on webRouter, this prevents problem with accessing sessions in locals, they should also only be used on web routes not api routes --- .../infrastructure/ExpressLocals.coffee | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index e11d22db94..e0bb0724e1 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -40,37 +40,37 @@ for path in [ module.exports = (app, webRouter, apiRouter)-> - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.session = req.session next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.jsPath = jsPath next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.settings = Settings next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.translate = (key, vars = {}) -> vars.appName = Settings.appName req.i18n.translate(key, vars) res.locals.currentUrl = req.originalUrl next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.getSiteHost = -> Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2) next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.formatProjectPublicAccessLevel = (privilegeLevel)-> formatedPrivileges = private:"Private", readOnly:"Public: Read Only", readAndWrite:"Public: Read and Write" return formatedPrivileges[privilegeLevel] || "Private" next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.buildReferalUrl = (referal_medium) -> url = Settings.siteUrl if req.session? and req.session.user? and req.session.user.referal_id? @@ -98,12 +98,12 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.csrfToken = req?.csrfToken() next() - app.use (req, res, next) -> + webRouter.use (req, res, next) -> res.locals.getReqQueryParam = (field)-> return req.query?[field] next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.fingerprint = (path) -> if fingerprints[path]? return fingerprints[path] @@ -112,16 +112,16 @@ module.exports = (app, webRouter, apiRouter)-> return "" next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.formatPrice = SubscriptionFormatters.formatPrice next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.externalAuthenticationSystemUsed = -> Settings.ldap? next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> if req.session.user? res.locals.user = email: req.session.user.email @@ -139,34 +139,34 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.sentryPublicDSN = Settings.sentry?.publicDSN next() - app.use (req, res, next) -> + webRouter.use (req, res, next) -> if req.query? and req.query.scribtex_path? res.locals.lookingForScribtex = true res.locals.scribtexPath = req.query.scribtex_path next() - app.use (req, res, next) -> + webRouter.use (req, res, next) -> res.locals.nav = Settings.nav res.locals.templates = Settings.templateLinks next() - app.use (req, res, next) -> + webRouter.use (req, res, next) -> SystemMessageManager.getMessages (error, messages = []) -> res.locals.systemMessages = messages next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.query = req.query next() - app.use (req, res, next)-> + webRouter.use (req, res, next)-> subdomain = _.find Settings.i18n.subdomainLang, (subdomain)-> subdomain.lngCode == req.showUserOtherLng and !subdomain.hide res.locals.recomendSubdomain = subdomain res.locals.currentLngCode = req.lng next() - app.use (req, res, next) -> + webRouter.use (req, res, next) -> if Settings.reloadModuleViewsOnEachRequest Modules.loadViewIncludes() res.locals.moduleIncludes = Modules.moduleIncludes From 4f0b922a5da04aefdbae2e9fdbb145a451858297 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 30 Jun 2015 16:37:21 +0100 Subject: [PATCH 15/57] changed name used when project or file uploaded, this changed when we started using https://github.com/expressjs/multer * originalname - Name of the file on the user's computer * name - Renamed file name --- .../coffee/Features/Uploads/ProjectUploadController.coffee | 7 ++++--- .../coffee/Uploads/ProjectUploadControllerTests.coffee | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee index 1fa00e3f01..83a8db5c2d 100644 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee @@ -9,8 +9,8 @@ module.exports = ProjectUploadController = uploadProject: (req, res, next) -> timer = new metrics.Timer("project-upload") user_id = req.session.user._id - {name, path} = req.files.qqfile - name = Path.basename(name, ".zip") + {originalname, path} = req.files.qqfile + name = Path.basename(originalname, ".zip") ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) -> fs.unlink path, -> timer.done() @@ -27,7 +27,8 @@ module.exports = ProjectUploadController = uploadFile: (req, res, next) -> timer = new metrics.Timer("file-upload") - {name, path} = req.files.qqfile + name = req.files.qqfile.originalname + path = req.files.qqfile.path project_id = req.params.Project_id folder_id = req.query.folder_id if !name? or name.length == 0 or name.length > 150 diff --git a/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee b/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee index 9ca0dccc30..dd4240d1b8 100644 --- a/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Uploads/ProjectUploadControllerTests.coffee @@ -29,7 +29,7 @@ describe "ProjectUploadController", -> @req.files = qqfile: path: @path - name: @name + originalname: @name @req.session = user: _id: @user_id @@ -102,7 +102,7 @@ describe "ProjectUploadController", -> @req.files = qqfile: path: @path - name: @name + originalname: @name @req.params = Project_id: @project_id @req.query = @@ -173,7 +173,7 @@ describe "ProjectUploadController", -> describe "with a bad request", -> beforeEach -> - @req.files.qqfile.name = "" + @req.files.qqfile.originalname = "" @ProjectUploadController.uploadFile @req, @res it "should return a a non success response", -> From 7fd29b18a806473fcd2cfc63fdd3bc870f30551d Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 1 Jul 2015 12:08:57 +0100 Subject: [PATCH 16/57] destroy users session before creating a new one for them after login session changed to prevent against fixation attacks --- .../Features/Authentication/AuthenticationController.coffee | 1 + .../Authentication/AuthenticationControllerTests.coffee | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 5b934e4f25..c992fec578 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -154,6 +154,7 @@ module.exports = AuthenticationController = # Regenerate the session to get a new sessionID (cookie value) to # protect against session fixation attacks oldSession = req.session + req.session.destroy() req.sessionStore.generate(req) for key, value of oldSession req.session[key] = value diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 485aeba475..10d0e87b12 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -423,6 +423,7 @@ describe "AuthenticationController", -> beforeEach -> @req.session = save: sinon.stub().callsArg(0) + destroy : sinon.stub() @req.sessionStore = generate: sinon.stub() @AuthenticationController.establishUserSession @req, @user, @callback @@ -435,6 +436,9 @@ describe "AuthenticationController", -> @req.session.user.referal_id.should.equal @user.referal_id @req.session.user.isAdmin.should.equal @user.isAdmin + it "should destroy the session", -> + @req.session.destroy.called.should.equal true + it "should regenerate the session to protect against session fixation", -> @req.sessionStore.generate.calledWith(@req).should.equal true From e6a670533defc198ec2d382053db33d5ee38cef1 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 7 May 2015 15:08:11 +0100 Subject: [PATCH 17/57] added default mongoose connection --- .../app/coffee/infrastructure/Mongoose.coffee | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 services/web/app/coffee/infrastructure/Mongoose.coffee diff --git a/services/web/app/coffee/infrastructure/Mongoose.coffee b/services/web/app/coffee/infrastructure/Mongoose.coffee new file mode 100644 index 0000000000..7fbf64fd71 --- /dev/null +++ b/services/web/app/coffee/infrastructure/Mongoose.coffee @@ -0,0 +1,16 @@ +mongoose = require('mongoose') +Settings = require 'settings-sharelatex' +logger = require('logger-sharelatex') + +mongoose.connect(Settings.mongo.url, server: poolSize: 10) + +mongoose.connection.on 'connected', () -> + logger.log {url:Settings.mongo.url}, 'mongoose default connection open' + +mongoose.connection.on 'error', (err) -> + logger.err err:err, 'mongoose error on default connection'; + +mongoose.connection.on 'disconnected', () -> + logger.log 'mongoose default connection disconnected' + +module.exports = mongoose From 56346ad88c26a0b2f8bfa3b7f9cf3f0d89f4bc9e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 1 Jul 2015 15:44:46 +0100 Subject: [PATCH 18/57] remove analytics router and fixed bad package.json --- services/web/app/coffee/router.coffee | 1 - services/web/package.json | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index d9ff55a0a3..98c15fcf43 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -64,7 +64,6 @@ module.exports = class Router PasswordResetRouter.apply(webRouter, apiRouter) StaticPagesRouter.apply(webRouter, apiRouter) RealTimeProxyRouter.apply(webRouter, apiRouter) - AnalyticsRouter.apply(webRouter, apiRouter) Modules.applyRouter(webRouter, apiRouter) diff --git a/services/web/package.json b/services/web/package.json index 5cb505a93e..fe8aab83a9 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -12,11 +12,8 @@ "dependencies": { "archiver": "0.9.0", "async": "0.6.2", -<<<<<<< HEAD -======= "base64-stream": "^0.1.2", "basic-auth-connect": "^1.0.0", ->>>>>>> 90b15d3... v1 of express4 conversion "bcrypt": "0.8.3", "body-parser": "^1.13.1", "bufferedstream": "1.6.0", @@ -31,22 +28,14 @@ "ldapjs": "^0.7.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.0.0", "lynx": "0.1.1", -<<<<<<< HEAD -======= "marked": "^0.3.3", "method-override": "^2.3.3", ->>>>>>> 90b15d3... v1 of express4 conversion "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.1.0", "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", "mongoose": "3.8.28", -<<<<<<< HEAD -======= - "mongoose-auto-increment": "3.0.2", "multer": "^0.1.8", - "mysql": "^2.7.0", ->>>>>>> 90b15d3... v1 of express4 conversion "node-uuid": "1.4.1", "nodemailer": "0.6.1", "optimist": "0.6.1", From 8020cd8f47f291f71941e111878976be453d1e1a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 2 Jul 2015 12:09:08 +0100 Subject: [PATCH 19/57] removed tpds from settings.defaults.coffee, if not set updates are now not queued --- .../TpdsUpdateSender.coffee | 5 ++ services/web/config/settings.defaults.coffee | 2 - .../TpdsUpdateSenderTests.coffee | 46 ++++++++++++++----- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index 4c96e29b59..f2d5bb94d1 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -15,10 +15,15 @@ buildPath = (user_id, project_name, filePath)-> +tpdsworkerEnabled = -> settings.apis.tpdsworker?.url? +if !tpdsworkerEnabled() + logger.log "tpdsworker is not enabled, request will not be sent to it" module.exports = TpdsUpdateSender = _enqueue: (group, method, job, callback)-> + if !tpdsworkerEnabled() + return callback() opts = uri:"#{settings.apis.tpdsworker.url}/enqueue/web_to_tpds_http_requests" json : diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 12b174e938..ba32e7c4d0 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -68,8 +68,6 @@ module.exports = thirdPartyDataStore: url : "http://localhost:3002" emptyProjectFlushDelayMiliseconds: 5 * seconds - tpdsworker: - url: "http://localhost:3030" tags: url :"http://localhost:3012" spelling: diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index 2957c8b48a..cd84080d17 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee @@ -23,20 +23,44 @@ describe 'TpdsUpdateSender', -> project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]} @Project = findById:sinon.stub().callsArgWith(2, null, project) @docstoreUrl = "docstore.sharelatex.env" + @request = sinon.stub().returns(pipe:->) + @settings = + siteUrl:siteUrl + httpAuthSiteUrl:httpAuthSiteUrl, + apis: + thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl} + filestore: + url: filestoreUrl + docstore: + pubUrl: @docstoreUrl @updateSender = SandboxedModule.require modulePath, requires: - "settings-sharelatex": - siteUrl:siteUrl - httpAuthSiteUrl:httpAuthSiteUrl, - apis: - thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl} - filestore: - url: filestoreUrl - docstore: - pubUrl: @docstoreUrl - + "settings-sharelatex": @settings "logger-sharelatex":{log:->} '../../models/Project': Project:@Project - 'request':->{pipe:->} + 'request':@request + + describe "_enqueue", -> + + it "should not call request if there is no tpdsworker url", (done)-> + @updateSender._enqueue null, null, null, (err)=> + @request.called.should.equal false + done() + + it "should post the message to the tpdsworker", (done)-> + @settings.apis.tpdsworker = url:"www.tpdsworker.env" + group = "myproject" + method = "somemethod" + job = "do something" + @request.callsArgWith(1) + @updateSender._enqueue group, method, job, (err)=> + args = @request.args[0][0] + args.json.group.should.equal group + args.json.job.should.equal job + args.json.method.should.equal method + args.uri.should.equal "www.tpdsworker.env/enqueue/web_to_tpds_http_requests" + done() + + describe 'sending updates', -> From 258a8e3f04ffa00049a53f9e1778fa0e1038d6fd Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Jul 2015 11:30:02 +0100 Subject: [PATCH 20/57] added picture of kiri and geri --- services/web/public/img/about/geri.jpg | Bin 0 -> 9212 bytes services/web/public/img/about/kiri_channon.jpg | Bin 0 -> 19387 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/web/public/img/about/geri.jpg create mode 100644 services/web/public/img/about/kiri_channon.jpg diff --git a/services/web/public/img/about/geri.jpg b/services/web/public/img/about/geri.jpg new file mode 100644 index 0000000000000000000000000000000000000000..abf3af8e8125f740b0a12a1c0b8f92162e49fb08 GIT binary patch literal 9212 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 literal 0 HcmV?d00001 diff --git a/services/web/public/img/about/kiri_channon.jpg b/services/web/public/img/about/kiri_channon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..81f83d2d18c80eb1d7119227f285fe5a4b924982 GIT binary patch literal 19387 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 B=N|w7 literal 0 HcmV?d00001 From 9a49ce4a0eb5ac3698b20f8662e0c43b5102a5c4 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Jul 2015 12:58:02 +0100 Subject: [PATCH 21/57] removed extra req.session.destroy --- .../web/app/coffee/Features/Project/ProjectApiController.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectApiController.coffee b/services/web/app/coffee/Features/Project/ProjectApiController.coffee index b2214ccf8f..d62e40ad05 100644 --- a/services/web/app/coffee/Features/Project/ProjectApiController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectApiController.coffee @@ -10,6 +10,5 @@ module.exports = if err? logger.log err:err, project_id:project_id, "something went wrong getting project details" return res.send 500 - req.session.destroy() res.json(projDetails) From 39df8964cfd9c46603330b6b269b841c4a68cc31 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Jul 2015 13:29:10 +0100 Subject: [PATCH 22/57] added route that got lost in merge --- services/web/app/coffee/router.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 98c15fcf43..a7240e27d3 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -133,7 +133,8 @@ module.exports = class Router # New 'stable' /internal API end points apiRouter.get '/internal/project/:project_id', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails apiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject - + apiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf + webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = From 9028bcf830b00e91ec069c3aaafcefa8184b5305 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Jul 2015 14:34:59 +0100 Subject: [PATCH 23/57] set body parser limit to 2mb --- services/web/app/coffee/infrastructure/Server.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 98cd7992dd..8db396bca5 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -57,8 +57,8 @@ Modules.loadViewIncludes app -app.use bodyParser.urlencoded({ extended: true }) -app.use bodyParser.json() +app.use bodyParser.urlencoded({ extended: true, limit: "2mb"}) +app.use bodyParser.json({limit: "2mb"}) app.use multer(dest: Settings.path.uploadFolder) app.use methodOverride() From 3ecf201edaa759e00cc9046ce99fe28b172b83ae Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 8 Jul 2015 16:56:38 +0100 Subject: [PATCH 24/57] send -> sendStatus --- .../Features/Chat/ChatController.coffee | 4 ++-- .../CollaboratorsController.coffee | 4 ++-- .../Features/Compile/CompileController.coffee | 4 ++-- .../Documents/DocumentController.coffee | 2 +- .../Editor/EditorHttpController.coffee | 14 +++++------ .../FileStore/FileStoreController.coffee | 4 ++-- .../HealthCheck/HealthCheckController.coffee | 4 ++-- .../PasswordResetController.coffee | 6 ++--- .../Project/ProjectApiController.coffee | 2 +- .../Features/Project/ProjectController.coffee | 20 ++++++++-------- .../ServerAdmin/AdminController.coffee | 12 +++++----- .../Spelling/SpellingController.coffee | 2 +- .../SubscriptionController.coffee | 12 +++++----- .../SubscriptionGroupController.coffee | 6 ++--- .../ThirdPartyDataStore/TpdsController.coffee | 12 +++++----- .../Features/User/UserController.coffee | 12 +++++----- services/web/app/coffee/router.coffee | 2 +- .../CollaboratorsControllerTests.coffee | 4 ++-- .../Compile/CompileControllerTests.coffee | 4 ++-- .../Editor/EditorHttpControllerTests.coffee | 15 ++++++------ .../PasswordResetControllerTests.coffee | 10 ++++---- .../Project/ProjectApiControllerTests.coffee | 2 +- .../Project/ProjectControllerTests.coffee | 24 +++++++++---------- .../SubscriptionControllerTests.coffee | 22 ++++++++--------- .../SubscriptionGroupControllerTests.coffee | 2 +- .../TpdsControllerTests.coffee | 12 +++++----- .../coffee/User/UserControllerTests.coffee | 18 +++++++------- .../coffee/helpers/MockResponse.coffee | 13 ++++++++++ 28 files changed, 131 insertions(+), 117 deletions(-) diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index 7df835a1f8..ef3cbf94fc 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -12,7 +12,7 @@ module.exports = ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)-> if err? logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api" - return res.send(500) + return res.sendStatus(500) EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)-> res.send() @@ -23,7 +23,7 @@ module.exports = ChatHandler.getMessages project_id, query, (err, messages)-> if err? logger.err err:err, query:query, "problem getting messages from chat api" - return res.send 500 + return res.sendStatus 500 logger.log length:messages?.length, "sending messages to client" res.set 'Content-Type', 'application/json' res.send messages diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index e956b7f6bf..d51a604637 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -18,7 +18,7 @@ module.exports = CollaboratorsController = return next(new Error("User should be logged in")) CollaboratorsHandler.removeUserFromProject req.params.project_id, user_id, (error) -> return next(error) if error? - res.send 204 + res.sendStatus 204 addUserToProject: (req, res, next) -> project_id = req.params.Project_id @@ -32,7 +32,7 @@ module.exports = CollaboratorsController = user_id = req.params.user_id EditorController.removeUserFromProject project_id, user_id, (error)-> return next(error) if error? - res.send 204 + res.sendStatus 204 _formatCollaborators: (project, callback = (error, collaborators) ->) -> collaborators = [] diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 9dd52b4e66..667accd65a 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -51,14 +51,14 @@ module.exports = CompileController = project_id = req.params.Project_id CompileManager.deleteAuxFiles project_id, (error) -> return next(error) if error? - res.send(200) + res.sendStatus(200) compileAndDownloadPdf: (req, res, next)-> project_id = req.params.project_id CompileManager.compile project_id, null, {}, (err)-> if err? logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf" - res.send 500 + res.sendStatus 500 url = "/project/#{project_id}/output/output.pdf" CompileController.proxyToClsi project_id, url, req, res, next diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 1170da2ef4..4ac575af58 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -26,7 +26,7 @@ module.exports = logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" return next(error) logger.log doc_id:doc_id, project_id:project_id, "finished receiving set document request from api (docupdater)" - res.send 200 + res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 848d33bdfb..2619316e6d 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -49,7 +49,7 @@ module.exports = EditorHttpController = name = req.body.name if !name? - return res.send 400 # Malformed request + return res.sendStatus 400 # Malformed request logger.log project_id: project_id, doc_id: doc_id, "restoring doc" ProjectEntityHandler.restoreDoc project_id, doc_id, name, (err, doc, folder_id) => @@ -68,7 +68,7 @@ module.exports = EditorHttpController = name = req.body.name parent_folder_id = req.body.parent_folder_id if !EditorHttpController._nameIsAcceptableLength(name) - return res.send 400 + return res.sendStatus 400 EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) -> return next(error) if error? res.json doc @@ -78,7 +78,7 @@ module.exports = EditorHttpController = name = req.body.name parent_folder_id = req.body.parent_folder_id if !EditorHttpController._nameIsAcceptableLength(name) - return res.send 400 + return res.sendStatus 400 EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) -> return next(error) if error? res.json doc @@ -89,10 +89,10 @@ module.exports = EditorHttpController = entity_type = req.params.entity_type name = req.body.name if !EditorHttpController._nameIsAcceptableLength(name) - return res.send 400 + return res.sendStatus 400 EditorController.renameEntity project_id, entity_id, entity_type, name, (error) -> return next(error) if error? - res.send 204 + res.sendStatus 204 moveEntity: (req, res, next) -> project_id = req.params.Project_id @@ -101,7 +101,7 @@ module.exports = EditorHttpController = folder_id = req.body.folder_id EditorController.moveEntity project_id, entity_id, folder_id, entity_type, (error) -> return next(error) if error? - res.send 204 + res.sendStatus 204 deleteDoc: (req, res, next)-> req.params.entity_type = "doc" @@ -121,6 +121,6 @@ module.exports = EditorHttpController = entity_type = req.params.entity_type EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) -> return next(error) if error? - res.send 204 + res.sendStatus 204 diff --git a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee index 8182268d8e..85ca986eff 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee @@ -12,10 +12,10 @@ module.exports = ProjectLocator.findElement {project_id: project_id, element_id: file_id, type: "file"}, (err, file)-> if err? logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error finding element for downloading file" - return res.send 500 + return res.sendStatus 500 FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)-> if err? logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error getting file stream for downloading file" - return res.send 500 + return res.sendStatus 500 res.setHeader("Content-Disposition", "attachment; filename=#{file.name}") stream.pipe res \ No newline at end of file diff --git a/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee b/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee index 97be776549..5ed5b85e08 100644 --- a/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee +++ b/services/web/app/coffee/Features/HealthCheck/HealthCheckController.coffee @@ -32,9 +32,9 @@ module.exports = HealthCheckController = checkRedis: (req, res, next)-> if redisCheck.isAlive() - res.send 200 + res.sendStatus 200 else - res.send 500 + res.sendStatus 500 Reporter = (res) -> (runner) -> diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index 5c5903466e..06bd4ba023 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee @@ -23,7 +23,7 @@ module.exports = if err? res.send 500, {message:err?.message} else if exists - res.send 200 + res.sendStatus 200 else res.send 404, {message: req.i18n.translate("cant_find_email")} @@ -35,10 +35,10 @@ module.exports = setNewUserPassword: (req, res)-> {passwordResetToken, password} = req.body if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 - return res.send 400 + return res.sendStatus 400 PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) -> return next(err) if err? if found - res.send 200 + res.sendStatus 200 else res.send 404, {message: req.i18n.translate("password_reset_token_expired")} \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectApiController.coffee b/services/web/app/coffee/Features/Project/ProjectApiController.coffee index d62e40ad05..b16991ac62 100644 --- a/services/web/app/coffee/Features/Project/ProjectApiController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectApiController.coffee @@ -9,6 +9,6 @@ module.exports = ProjectDetailsHandler.getDetails project_id, (err, projDetails)-> if err? logger.log err:err, project_id:project_id, "something went wrong getting project details" - return res.send 500 + return res.sendStatus 500 res.json(projDetails) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 50a8e91670..5466fe1bf9 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -44,7 +44,7 @@ module.exports = ProjectController = async.series jobs, (error) -> return next(error) if error? - res.send(204) + res.sendStatus(204) deleteProject: (req, res) -> project_id = req.params.Project_id @@ -58,18 +58,18 @@ module.exports = ProjectController = doDelete project_id, (err)-> if err? - res.send 500 + res.sendStatus 500 else - res.send 200 + res.sendStatus 200 restoreProject: (req, res) -> project_id = req.params.Project_id logger.log project_id:project_id, "received request to restore project" projectDeleter.restoreProject project_id, (err)-> if err? - res.send 500 + res.sendStatus 500 else - res.send 200 + res.sendStatus 200 cloneProject: (req, res, next)-> metrics.inc "cloned-project" @@ -99,7 +99,7 @@ module.exports = ProjectController = ], (err, project)-> if err? logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project" - res.send 500 + res.sendStatus 500 else logger.log project: project, user: user, name: projectName, templateType: template, "created project" res.send {project_id:project._id} @@ -109,13 +109,13 @@ module.exports = ProjectController = project_id = req.params.Project_id newName = req.body.newProjectName if newName.length > 150 - return res.send 400 + return res.sendStatus 400 editorController.renameProject project_id, newName, (err)-> if err? logger.err err:err, project_id:project_id, newName:newName, "problem renaming project" - res.send 500 + res.sendStatus 500 else - res.send 200 + res.sendStatus 200 projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") @@ -196,7 +196,7 @@ module.exports = ProjectController = SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)-> if !canAccess - return res.send 401 + return res.sendStatus 401 if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? allowedFreeTrial = !!subscription.freeTrial.allowed || true diff --git a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee index e875413908..005f2d23d3 100755 --- a/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee +++ b/services/web/app/coffee/Features/ServerAdmin/AdminController.coffee @@ -50,12 +50,12 @@ module.exports = AdminController = dissconectAllUsers: (req, res)=> logger.warn "disconecting everyone" EditorRealTimeController.emitToAll 'forceDisconnect', "Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue." - res.send(200) + res.sendStatus(200) closeEditor : (req, res)-> logger.warn "closing editor" Settings.editorIsOpen = req.body.isOpen - res.send(200) + res.sendStatus(200) writeAllToMongo : (req, res)-> logger.log "writing all docs to mongo" @@ -74,19 +74,19 @@ module.exports = AdminController = flushProjectToTpds: (req, res)-> projectEntityHandler.flushProjectToThirdPartyDataStore req.body.project_id, (err)-> - res.send 200 + res.sendStatus 200 pollDropboxForUser: (req, res)-> user_id = req.body.user_id TpdsUpdateSender.pollDropboxForUser user_id, () -> - res.send 200 + res.sendStatus 200 createMessage: (req, res, next) -> SystemMessageManager.createMessage req.body.content, (error) -> return next(error) if error? - res.send 200 + res.sendStatus 200 clearMessages: (req, res, next) -> SystemMessageManager.clearMessages (error) -> return next(error) if error? - res.send 200 + res.sendStatus 200 diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee index 0a1c8f0917..f87504e5d0 100644 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -11,4 +11,4 @@ module.exports = SpellingController = getReq.pipe(res) getReq.on "error", (error) -> logger.error err: error, "Spelling API error" - res.send 500 + res.sendStatus 500 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index ea03458093..e3b3747baa 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -147,8 +147,8 @@ module.exports = SubscriptionController = SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)-> if err? logger.err err:err, user_id:user._id, "something went wrong creating subscription" - return res.send 500 - res.send 201 + return res.sendStatus 500 + res.sendStatus 201 successful_subscription: (req, res)-> SecurityManager.getCurrentUser req, (error, user) => @@ -191,9 +191,9 @@ module.exports = SubscriptionController = if req.body? and req.body["expired_subscription_notification"]? recurlySubscription = req.body["expired_subscription_notification"].subscription SubscriptionHandler.recurlyCallback recurlySubscription, -> - res.send 200 + res.sendStatus 200 else - res.send 200 + res.sendStatus 200 renderUpgradeToAnnualPlanPage: (req, res)-> SecurityManager.getCurrentUser req, (error, user) -> @@ -221,9 +221,9 @@ module.exports = SubscriptionController = SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)-> if err? logger.err err:err, user_id:user._id, "error updating subscription" - res.send 500 + res.sendStatus 500 else - res.send 200 + res.sendStatus 200 recurlyNotificationParser: (req, res, next) -> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 8d8bd59bde..3bf8d4a631 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -62,9 +62,9 @@ module.exports = return ErrorsController.notFound(req, res) SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)-> if err? - res.send 500 + res.sendStatus 500 else - res.send 200 + res.sendStatus 200 completeJoin: (req, res)-> subscription_id = req.params.subscription_id @@ -74,7 +74,7 @@ module.exports = if err? and err == "token_not_found" res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true" else if err? - res.send 500 + res.sendStatus 500 else res.redirect "/user/subscription/#{subscription_id}/group/successful-join" diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee index 3dbb30f75c..f256760808 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee @@ -17,10 +17,10 @@ module.exports = logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], "sending response that tpdsUpdate has been completed" if err? logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" - res.send(500) + res.sendStatus(500) else logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed" - res.send 200 + res.sendStatus 200 deleteUpdate: (req, res)-> @@ -31,10 +31,10 @@ module.exports = tpdsUpdateHandler.deleteUpdate user_id, projectName, filePath, source, (err)-> if err? logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds" - res.send(500) + res.sendStatus(500) else logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed" - res.send 200 + res.sendStatus 200 # updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we # can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy @@ -47,7 +47,7 @@ module.exports = logger.log project_id: project_id, path: path, source: source, "received project contents update" UpdateMerger.mergeUpdate project_id, path, req, source, (error) -> return next(error) if error? - res.send(200) + res.sendStatus(200) deleteProjectContents: (req, res, next = (error) ->) -> {project_id} = req.params @@ -56,7 +56,7 @@ module.exports = logger.log project_id: project_id, path: path, source: source, "received project contents delete request" UpdateMerger.deleteUpdate project_id, path, source, (error) -> return next(error) if error? - res.send(200) + res.sendStatus(200) parseParams: parseParams = (req)-> path = req.params[0] diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 643404a14d..468832ca98 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -21,7 +21,7 @@ module.exports = UserDeleter.deleteUser user_id, (err)-> if !err? req.session?.destroy() - res.send(200) + res.sendStatus(200) unsubscribe: (req, res)-> UserLocator.findById req.session.user._id, (err, user)-> @@ -34,7 +34,7 @@ module.exports = User.findById user_id, (err, user)-> if err? or !user? logger.err err:err, user_id:user_id, "problem updaing user settings" - return res.send 500 + return res.sendStatus 500 if req.body.first_name? user.first_name = req.body.first_name.trim() @@ -59,9 +59,9 @@ module.exports = user.save (err)-> newEmail = req.body.email?.trim().toLowerCase() if !newEmail? or newEmail == user.email - return res.send 200 + return res.sendStatus 200 else if newEmail.indexOf("@") == -1 - return res.send(400) + return res.sendStatus(400) else UserUpdater.changeEmailAddress user_id, newEmail, (err)-> if err? @@ -71,7 +71,7 @@ module.exports = else message = req.i18n.translate("problem_changing_email_address") return res.send 500, {message:message} - res.send(200) + res.sendStatus(200) logout : (req, res)-> metrics.inc "user.logout" @@ -84,7 +84,7 @@ module.exports = register : (req, res, next = (error) ->)-> email = req.body.email if !email? or email == "" - res.send 422 # Unprocessable Entity + res.sendStatus 422 # Unprocessable Entity return logger.log {email}, "registering new user" UserRegistrationHandler.registerNewUser { diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index a7240e27d3..4d0c8279df 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -212,6 +212,6 @@ module.exports = class Router webRouter.post '/error/client', (req, res, next) -> logger.error err: req.body.error, meta: req.body.meta, "client side error" - res.send(204) + res.sendStatus(204) webRouter.get '*', ErrorController.notFound diff --git a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee index 11659296cb..c2e8f45685 100644 --- a/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Collaborators/CollaboratorsControllerTests.coffee @@ -91,7 +91,7 @@ describe "CollaboratorsController", -> @req.params = Project_id: @project_id = "project-id-123" user_id: @user_id = "user-id-123" - @res.send = sinon.stub() + @res.sendStatus = sinon.stub() @EditorController.removeUserFromProject = sinon.stub().callsArg(2) @CollaboratorsController.removeUserFromProject @req, @res @@ -101,7 +101,7 @@ describe "CollaboratorsController", -> .should.equal true it "should send the back a success response", -> - @res.send.calledWith(204).should.equal true + @res.sendStatus.calledWith(204).should.equal true describe "_formatCollaborators", -> diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index e3fd444870..08299812fe 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee @@ -321,7 +321,7 @@ describe "CompileController", -> @CompileManager.deleteAuxFiles = sinon.stub().callsArg(1) @req.params = Project_id: @project_id - @res.send = sinon.stub() + @res.sendStatus = sinon.stub() @CompileController.deleteAuxFiles @req, @res, @next it "should proxy to the CLSI", -> @@ -330,7 +330,7 @@ describe "CompileController", -> .should.equal true it "should return a 200", -> - @res.send + @res.sendStatus .calledWith(200) .should.equal true diff --git a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee index e740e35882..ab003208c2 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorHttpControllerTests.coffee @@ -24,6 +24,7 @@ describe "EditorHttpController", -> @req = {} @res = send: sinon.stub() + sendStatus: sinon.stub() json: sinon.stub() @callback = sinon.stub() @@ -190,7 +191,7 @@ describe "EditorHttpController", -> @EditorHttpController.addDoc @req, @res it "should send back a bad request status code", -> - @res.send.calledWith(400).should.equal true + @res.sendStatus.calledWith(400).should.equal true describe "addFolder", -> beforeEach -> @@ -223,7 +224,7 @@ describe "EditorHttpController", -> @EditorHttpController.addFolder @req, @res it "should send back a bad request status code", -> - @res.send.calledWith(400).should.equal true + @res.sendStatus.calledWith(400).should.equal true describe "renameEntity", -> @@ -243,7 +244,7 @@ describe "EditorHttpController", -> .should.equal true it "should send back a success response", -> - @res.send.calledWith(204).should.equal true + @res.sendStatus.calledWith(204).should.equal true describe "renameEntity with long name", -> beforeEach -> @@ -257,7 +258,7 @@ describe "EditorHttpController", -> @EditorHttpController.renameEntity @req, @res it "should send back a bad request status code", -> - @res.send.calledWith(400).should.equal true + @res.sendStatus.calledWith(400).should.equal true describe "rename entity with 0 length name", -> @@ -272,7 +273,7 @@ describe "EditorHttpController", -> @EditorHttpController.renameEntity @req, @res it "should send back a bad request status code", -> - @res.send.calledWith(400).should.equal true + @res.sendStatus.calledWith(400).should.equal true describe "moveEntity", -> @@ -292,7 +293,7 @@ describe "EditorHttpController", -> .should.equal true it "should send back a success response", -> - @res.send.calledWith(204).should.equal true + @res.sendStatus.calledWith(204).should.equal true describe "deleteEntity", -> beforeEach -> @@ -309,4 +310,4 @@ describe "EditorHttpController", -> .should.equal true it "should send back a success response", -> - @res.send.calledWith(204).should.equal true + @res.sendStatus.calledWith(204).should.equal true diff --git a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee index a6590b31da..efc4d9cbde 100644 --- a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee @@ -51,7 +51,7 @@ describe "PasswordResetController", -> it "should tell the handler to process that email", (done)-> @RateLimiter.addCount.callsArgWith(1, null, true) @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal true done() @@ -78,7 +78,7 @@ describe "PasswordResetController", -> @req.body.email = @email @RateLimiter.addCount.callsArgWith(1, null, true) @PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.toLowerCase()).should.equal true done() @@ -88,7 +88,7 @@ describe "PasswordResetController", -> it "should tell the user handler to reset the password", (done)-> @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true done() @@ -104,7 +104,7 @@ describe "PasswordResetController", -> it "should return 400 (Bad Request) if there is no password", (done)-> @req.body.password = "" @PasswordResetHandler.setNewUserPassword.callsArgWith(2) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 400 @PasswordResetHandler.setNewUserPassword.called.should.equal false done() @@ -113,7 +113,7 @@ describe "PasswordResetController", -> it "should return 400 (Bad Request) if there is no passwordResetToken", (done)-> @req.body.passwordResetToken = "" @PasswordResetHandler.setNewUserPassword.callsArgWith(2) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 400 @PasswordResetHandler.setNewUserPassword.called.should.equal false done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee index 322d61750e..9476a80019 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee @@ -36,7 +36,7 @@ describe 'Project api controller', -> it "should send a 500 if there is an error", (done)-> @ProjectDetailsHandler.getDetails.callsArgWith(1, "error") - @res.send = (resCode)=> + @res.sendStatus = (resCode)=> resCode.should.equal 500 done() @controller.getProjectDetails @req, @res diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 77bfcebd35..c5c14c4359 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -78,7 +78,7 @@ describe "ProjectController", -> @EditorController.renameProject = sinon.stub().callsArg(2) @req.body = name: @name = "New name" - @res.send = (code) => + @res.sendStatus = (code) => @EditorController.renameProject .calledWith(@project_id, @name) .should.equal true @@ -90,7 +90,7 @@ describe "ProjectController", -> @EditorController.setCompiler = sinon.stub().callsArg(2) @req.body = compiler: @compiler = "pdflatex" - @res.send = (code) => + @res.sendStatus = (code) => @EditorController.setCompiler .calledWith(@project_id, @compiler) .should.equal true @@ -102,7 +102,7 @@ describe "ProjectController", -> @EditorController.setSpellCheckLanguage = sinon.stub().callsArg(2) @req.body = spellCheckLanguage: @languageCode = "fr" - @res.send = (code) => + @res.sendStatus = (code) => @EditorController.setSpellCheckLanguage .calledWith(@project_id, @languageCode) .should.equal true @@ -114,7 +114,7 @@ describe "ProjectController", -> @EditorController.setPublicAccessLevel = sinon.stub().callsArg(2) @req.body = publicAccessLevel: @publicAccessLevel = "readonly" - @res.send = (code) => + @res.sendStatus = (code) => @EditorController.setPublicAccessLevel .calledWith(@project_id, @publicAccessLevel) .should.equal true @@ -126,7 +126,7 @@ describe "ProjectController", -> @EditorController.setRootDoc = sinon.stub().callsArg(2) @req.body = rootDocId: @rootDocId = "root-doc-id" - @res.send = (code) => + @res.sendStatus = (code) => @EditorController.setRootDoc .calledWith(@project_id, @rootDocId) .should.equal true @@ -136,7 +136,7 @@ describe "ProjectController", -> describe "deleteProject", -> it "should tell the project deleter to archive when forever=false", (done)-> - @res.send = (code)=> + @res.sendStatus = (code)=> @ProjectDeleter.archiveProject.calledWith(@project_id).should.equal true code.should.equal 200 done() @@ -144,7 +144,7 @@ describe "ProjectController", -> it "should tell the project deleter to delete when forever=true", (done)-> @req.query = forever: "true" - @res.send = (code)=> + @res.sendStatus = (code)=> @ProjectDeleter.deleteProject.calledWith(@project_id).should.equal true code.should.equal 200 done() @@ -152,7 +152,7 @@ describe "ProjectController", -> describe "restoreProject", -> it "should tell the project deleter", (done)-> - @res.send = (code)=> + @res.sendStatus = (code)=> @ProjectDeleter.restoreProject.calledWith(@project_id).should.equal true code.should.equal 200 done() @@ -244,7 +244,7 @@ describe "ProjectController", -> it "should call the editor controller", (done)-> @EditorController.renameProject.callsArgWith(2) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true done() @@ -252,7 +252,7 @@ describe "ProjectController", -> it "should send a 500 if there is a problem", (done)-> @EditorController.renameProject.callsArgWith(2, "problem") - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 500 @EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true done() @@ -260,7 +260,7 @@ describe "ProjectController", -> it "should return an error if the name is over 150 chars", (done)-> @req.body.newProjectName = "EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT" - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 400 done() @ProjectController.renameProject @req, @res @@ -317,7 +317,7 @@ describe "ProjectController", -> it "should not render the page if the project can not be accessed", (done)-> @SecurityManager.userCanAccessProject = sinon.stub().callsArgWith 2, false - @res.send = (resCode, opts)=> + @res.sendStatus = (resCode, opts)=> resCode.should.equal 401 done() @ProjectController.loadEditor @req, @res diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index 56e31c5f26..87cbc11671 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -285,9 +285,9 @@ describe "SubscriptionController sanboxed", -> describe "createSubscription", -> beforeEach (done)-> @res = - send:-> + sendStatus:-> done() - sinon.spy @res, "send" + sinon.spy @res, "sendStatus" @subscriptionDetails = card:"1234" cvv:"123" @@ -300,7 +300,7 @@ describe "SubscriptionController sanboxed", -> done() it "should redurect to the subscription page", (done)-> - @res.send.calledWith(201).should.equal true + @res.sendStatus.calledWith(201).should.equal true done() @@ -363,9 +363,9 @@ describe "SubscriptionController sanboxed", -> expired_subscription_notification: subscription: uuid: @activeRecurlySubscription.uuid - @res = send:-> + @res = sendStatus:-> done() - sinon.spy @res, "send" + sinon.spy @res, "sendStatus" @SubscriptionController.recurlyCallback @req, @res it "should tell the SubscriptionHandler to process the recurly callback", (done)-> @@ -374,7 +374,7 @@ describe "SubscriptionController sanboxed", -> it "should send a 200", (done)-> - @res.send.calledWith(200) + @res.sendStatus.calledWith(200) done() describe "with a non-actionable request", -> @@ -385,16 +385,16 @@ describe "SubscriptionController sanboxed", -> new_subscription_notification: subscription: uuid: @activeRecurlySubscription.uuid - @res = send:-> + @res = sendStatus:-> done() - sinon.spy @res, "send" + sinon.spy @res, "sendStatus" @SubscriptionController.recurlyCallback @req, @res it "should not call the subscriptionshandler", -> @SubscriptionHandler.recurlyCallback.called.should.equal false it "should respond with a 200 status", -> - @res.send.calledWith(200) + @res.sendStatus.calledWith(200) describe "renderUpgradeToAnnualPlanPage", -> @@ -442,7 +442,7 @@ describe "SubscriptionController sanboxed", -> @req.body = planName:"student" - @res.send = ()=> + @res.sendStatus = ()=> @SubscriptionHandler.updateSubscription.calledWith(@user, "student-annual", "STUDENTCODEHERE").should.equal true done() @@ -453,7 +453,7 @@ describe "SubscriptionController sanboxed", -> @req.body = planName:"collaborator" - @res.send = (url)=> + @res.sendStatus = (url)=> @SubscriptionHandler.updateSubscription.calledWith(@user, "collaborator-annual", "COLLABORATORCODEHERE").should.equal true done() diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee index 8c3181da03..dbaf751c70 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionGroupControllerTests.coffee @@ -127,7 +127,7 @@ describe "SubscriptionGroupController", -> it "should ask the SubscriptionGroupHandler to send the verification email", (done)-> res = - send : (statusCode)=> + sendStatus : (statusCode)=> statusCode.should.equal 200 @GroupHandler.sendVerificationEmail.calledWith(@subscription_id, @licenceName, @user_email).should.equal true done() diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee index 769582a95a..39248be52d 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsControllerTests.coffee @@ -28,7 +28,7 @@ describe 'TpdsController', -> headers: "x-sl-update-source": @source = "dropbox" @TpdsUpdateHandler.newUpdate = sinon.stub().callsArg(5) - res = send: => + res = sendStatus: => @TpdsUpdateHandler.newUpdate.calledWith(@user_id, "projectName","/here.txt", req, @source).should.equal true done() @TpdsController.mergeUpdate req, res @@ -43,7 +43,7 @@ describe 'TpdsController', -> headers: "x-sl-update-source": @source = "dropbox" @TpdsUpdateHandler.deleteUpdate = sinon.stub().callsArg(4) - res = send: => + res = sendStatus: => @TpdsUpdateHandler.deleteUpdate.calledWith(@user_id, "projectName", "/here.txt", @source).should.equal true done() @TpdsController.deleteUpdate req, res @@ -86,7 +86,7 @@ describe 'TpdsController', -> headers: "x-sl-update-source": @source = "github" @res = - send: sinon.stub() + sendStatus: sinon.stub() @TpdsController.updateProjectContents @req, @res @@ -96,7 +96,7 @@ describe 'TpdsController', -> .should.equal true it "should return a success", -> - @res.send.calledWith(200).should.equal true + @res.sendStatus.calledWith(200).should.equal true describe 'deleteProjectContents', -> @@ -111,7 +111,7 @@ describe 'TpdsController', -> headers: "x-sl-update-source": @source = "github" @res = - send: sinon.stub() + sendStatus: sinon.stub() @TpdsController.deleteProjectContents @req, @res @@ -121,5 +121,5 @@ describe 'TpdsController', -> .should.equal true it "should return a success", -> - @res.send.calledWith(200).should.equal true + @res.sendStatus.calledWith(200).should.equal true diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee index 3bc8a3ca46..e1883cb262 100644 --- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee @@ -78,7 +78,7 @@ describe "UserController", -> it "should delete the user", (done)-> - @res.send = (code)=> + @res.sendStatus = (code)=> @UserDeleter.deleteUser.calledWith(@user_id) code.should.equal 200 done() @@ -98,7 +98,7 @@ describe "UserController", -> it "should call save", (done)-> @req.body = {} - @res.send = (code)=> + @res.sendStatus = (code)=> @user.save.called.should.equal true done() @UserController.updateUserSettings @req, @res @@ -106,7 +106,7 @@ describe "UserController", -> it "should set the first name", (done)-> @req.body = first_name: "bobby " - @res.send = (code)=> + @res.sendStatus = (code)=> @user.first_name.should.equal "bobby" done() @UserController.updateUserSettings @req, @res @@ -114,7 +114,7 @@ describe "UserController", -> it "should set the role", (done)-> @req.body = role: "student" - @res.send = (code)=> + @res.sendStatus = (code)=> @user.role.should.equal "student" done() @UserController.updateUserSettings @req, @res @@ -122,7 +122,7 @@ describe "UserController", -> it "should set the institution", (done)-> @req.body = institution: "MIT" - @res.send = (code)=> + @res.sendStatus = (code)=> @user.institution.should.equal "MIT" done() @UserController.updateUserSettings @req, @res @@ -130,21 +130,21 @@ describe "UserController", -> it "should set some props on ace", (done)-> @req.body = theme: "something" - @res.send = (code)=> + @res.sendStatus = (code)=> @user.ace.theme.should.equal "something" done() @UserController.updateUserSettings @req, @res it "should send an error if the email is 0 len", (done)-> @req.body.email = "" - @res.send = (code)-> + @res.sendStatus = (code)-> code.should.equal 400 done() @UserController.updateUserSettings @req, @res it "should send an error if the email does not contain an @", (done)-> @req.body.email = "bob at something dot com" - @res.send = (code)-> + @res.sendStatus = (code)-> code.should.equal 400 done() @UserController.updateUserSettings @req, @res @@ -152,7 +152,7 @@ describe "UserController", -> it "should call the user updater with the new email and user _id", (done)-> @req.body.email = @newEmail.toUpperCase() @UserUpdater.changeEmailAddress.callsArgWith(2) - @res.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @UserUpdater.changeEmailAddress.calledWith(@user_id, @newEmail).should.equal true done() diff --git a/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee b/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee index ab78f248b9..f3a7f7a775 100644 --- a/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee +++ b/services/web/test/UnitTests/coffee/helpers/MockResponse.coffee @@ -22,6 +22,19 @@ class MockResponse @redirectedTo = url @callback() if @callback? + sendStatus: (status) -> + if arguments.length < 2 + if typeof status != "number" + body = status + status = 200 + @statusCode = status + @returned = true + if 200 <= status < 300 + @success = true + else + @success = false + @callback() if @callback? + send: (status, body) -> if arguments.length < 2 if typeof status != "number" From a786b623a85f0dc95ba370580c4e7dddf8e8c8aa Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 22 Jul 2015 01:06:23 +0100 Subject: [PATCH 25/57] added logging to help debug slow project list page loading --- .../web/app/coffee/Features/Project/ProjectController.coffee | 4 +++- services/web/app/coffee/models/Project.coffee | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 5466fe1bf9..79c850b04d 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -173,6 +173,7 @@ module.exports = ProjectController = user_id = 'openUser' project_id = req.params.Project_id + logger.log project_id:project_id, "loading editor" async.parallel { project: (cb)-> @@ -190,6 +191,7 @@ module.exports = ProjectController = if err? logger.err err:err, "error getting details for project page" return next err + logger.log project_id:project_id, "got db results for loading editor" project = results.project user = results.user subscription = results.subscription @@ -200,7 +202,7 @@ module.exports = ProjectController = if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt? allowedFreeTrial = !!subscription.freeTrial.allowed || true - + logger.log project_id:project_id, "rendering editor page" res.render 'project/editor', title: project.name priority_title: true diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 52901512c9..a658b42080 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -42,6 +42,7 @@ ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> this.findById project_or_id, fields, callback ProjectSchema.statics.findPopulatedById = (project_id, callback)-> + logger.log project_id:project_id, "findPopulatedById" this.find(_id: project_id ) .populate('collaberator_refs') .populate('readOnly_refs') @@ -54,6 +55,7 @@ ProjectSchema.statics.findPopulatedById = (project_id, callback)-> logger.err project_id:project_id, "something went wrong looking for project findPopulatedById, no project could be found" callback "not found" else + logger.log project_id:project_id, "finished findPopulatedById" callback(null, projects[0]) ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)-> From c12213b46b0f291f58cc9d3e013930fc64ccbe8f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 22 Jul 2015 10:38:28 +0100 Subject: [PATCH 26/57] added logging around load editor times --- .../web/app/coffee/Features/Project/ProjectController.coffee | 4 +++- .../coffee/Features/Subscription/SubscriptionLocator.coffee | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 79c850b04d..75621f05f1 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -182,7 +182,9 @@ module.exports = ProjectController = if user_id == 'openUser' cb null, defaultSettingsForAnonymousUser(user_id) else - User.findById user_id, cb + User.findById user_id, (err, user)-> + logger.log project_id:project_id, user_id:user_id, "got user" + cb err, user subscription: (cb)-> if user_id == 'openUser' return cb() diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index 52dbf6b835..08f5507b7f 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -10,7 +10,9 @@ module.exports = else if user_or_id? user_id = user_or_id logger.log user_id:user_id, "getting users subscription" - Subscription.findOne admin_id:user_id, callback + Subscription.findOne admin_id:user_id, (err, subscription)-> + logger.log user_id:user_id, "got users subscription" + callback(err, subscription) getMemberSubscriptions: (user_id, callback) -> logger.log user_id: user_id, "getting users group subscriptions" From 417fd4f5f537f722e7d9a2b99b4b87e5739465f2 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 22 Jul 2015 10:38:48 +0100 Subject: [PATCH 27/57] add logging to tell us how long since a project that is being opened was last updated --- .../web/app/coffee/Features/Project/ProjectController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 75621f05f1..55b59cb76f 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -193,11 +193,13 @@ module.exports = ProjectController = if err? logger.err err:err, "error getting details for project page" return next err - logger.log project_id:project_id, "got db results for loading editor" project = results.project user = results.user subscription = results.subscription + daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 + logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" + SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel)-> if !canAccess return res.sendStatus 401 From c5cc639bff0e0d6af2d7bf7310cbf1511baf82f8 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Sun, 2 Aug 2015 15:24:17 +0100 Subject: [PATCH 28/57] upgrade mongoose from 3.8.28 to 4.1.0 --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index fe8aab83a9..d9b074157f 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -34,7 +34,7 @@ "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", - "mongoose": "3.8.28", + "mongoose": "4.1.0", "multer": "^0.1.8", "node-uuid": "1.4.1", "nodemailer": "0.6.1", From 2bc003894b42818bdc223cce4c403f387d4e214a Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Sun, 2 Aug 2015 15:55:26 +0100 Subject: [PATCH 29/57] use latest version of metrics with commit id until ready to tag --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index d9b074157f..01cd6d9029 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -30,7 +30,7 @@ "lynx": "0.1.1", "marked": "^0.3.3", "method-override": "^2.3.3", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.1.0", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#0966b75a646720781c6accdca15832e675d7d3f2", "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", From 53dc0b63c8a138b0290b5317b6f53afbb79b39d7 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 4 Aug 2015 10:45:19 +0100 Subject: [PATCH 30/57] v1 of enago --- .../web/app/views/project/editor/left-menu.jade | 5 +++++ services/web/public/img/enago.png | Bin 0 -> 5829 bytes services/web/public/stylesheets/app/editor.less | 1 + .../web/public/stylesheets/app/editor/enago.less | 13 +++++++++++++ 4 files changed, 19 insertions(+) create mode 100644 services/web/public/img/enago.png create mode 100644 services/web/public/stylesheets/app/editor/enago.less diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index ecc4e78164..8c2ca82f87 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -48,6 +48,11 @@ aside#left-menu.full-size( h4() #{translate("sync")} != moduleIncludes("editorLeftMenu:sync", locals) + span(ng-show="!anonymous") + h4 #{translate("services")} + != moduleIncludes("editorLeftMenu:editing_services", locals) + + h4(ng-show="!anonymous") #{translate("settings")} form.settings(ng-controller="SettingsController", ng-show="!anonymous") .containter-fluid diff --git a/services/web/public/img/enago.png b/services/web/public/img/enago.png new file mode 100644 index 0000000000000000000000000000000000000000..879dd216c0013959aeae7a5d38e27edabccc2ebe GIT binary patch literal 5829 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 ZfuwU+3Xg3@>#vJt4OLy03T5li{{i76jPn2h literal 0 HcmV?d00001 diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 71b3cde0aa..ed8f36536c 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -3,6 +3,7 @@ @import "./editor/toolbar.less"; @import "./editor/left-menu.less"; @import "./editor/pdf.less"; +@import "./editor/enago.less"; @import "./editor/share.less"; @import "./editor/chat.less"; @import "./editor/binary-file.less"; diff --git a/services/web/public/stylesheets/app/editor/enago.less b/services/web/public/stylesheets/app/editor/enago.less new file mode 100644 index 0000000000..e31b693e69 --- /dev/null +++ b/services/web/public/stylesheets/app/editor/enago.less @@ -0,0 +1,13 @@ +.services { + h1, h2, h3, p { + text-shadow: 0 -1px 1px white; + } + h1, h2, h3, h4 { + color: @red; + } + + + hr.small { + margin:0px; + } +} \ No newline at end of file From a25373d53e5c8d4e010da457261b10b10857e702 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 6 Aug 2015 09:44:16 +0100 Subject: [PATCH 31/57] update metric-sharelatex to v1.2.0 for mongodb-core support --- services/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/package.json b/services/web/package.json index 01cd6d9029..86cfd3f583 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -30,7 +30,7 @@ "lynx": "0.1.1", "marked": "^0.3.3", "method-override": "^2.3.3", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#0966b75a646720781c6accdca15832e675d7d3f2", + "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.2.0", "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "0.18.2", From a0142d4415430ead4d73a8e8efbab4dd3a34de0f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 13 Aug 2015 22:40:28 +0100 Subject: [PATCH 32/57] added inactive and reactivate project logic --- .../Features/Docstore/DocstoreManager.coffee | 28 ++++++ .../InactiveProjectController.coffee | 11 +++ .../InactiveProjectManager.coffee | 53 ++++++++++++ .../Features/Project/ProjectController.coffee | 6 ++ .../Project/ProjectUpdateHandler.coffee | 22 +++++ services/web/app/coffee/models/Project.coffee | 2 + .../Docstore/DocstoreManagerTests.coffee | 41 ++++++++- .../InactiveProjectManagerTests.coffee | 85 +++++++++++++++++++ .../Project/ProjectControllerTests.coffee | 22 +++++ .../Project/ProjectUpdateHandlerTests.coffee | 35 +++++++- 10 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee create mode 100644 services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee create mode 100644 services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee index 1e23c1f70b..a755c47422 100644 --- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee +++ b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee @@ -67,3 +67,31 @@ module.exports = DocstoreManager = error = new Error("docstore api responded with non-success code: #{res.statusCode}") logger.error err: error, project_id: project_id, doc_id: doc_id, "error updating doc in docstore" callback(error) + + archiveProject: (project_id, callback)-> + url = "#{settings.apis.docstore.url}/project/#{project_id}/archive" + logger.log project_id:project_id, "archiving project in docstore" + request.post url, (err, res, docs) -> + if err? + logger.err err:err, project_id:project_id, "error archving project in docstore" + return callback(err) + if 200 <= res.statusCode < 300 + callback() + else + error = new Error("docstore api responded with non-success code: #{res.statusCode}") + logger.err err: error, project_id: project_id, "error archiving project in docstore" + return callback(error) + + unarchiveProject: (project_id, callback)-> + url = "#{settings.apis.docstore.url}/project/#{project_id}/unarchive" + logger.log project_id:project_id, "unarchiving project in docstore" + request.post url, (err, res, docs) -> + if err? + logger.err err:err, project_id:project_id, "error unarchiving project in docstore" + return callback(err) + if 200 <= res.statusCode < 300 + callback() + else + error = new Error("docstore api responded with non-success code: #{res.statusCode}") + logger.err err: error, project_id: project_id, "error unarchiving project in docstore" + return callback(error) \ No newline at end of file diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee new file mode 100644 index 0000000000..b37b43872f --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -0,0 +1,11 @@ +InactiveProjectManager = require("./InactiveProjectManager") + +module.exports = + + deactivateOldProjects: (req, res)-> + InactiveProjectManager.deactivateOldProjects 10, (err)-> + if err? + res.sendStatus(500) + else + res.sendStatus(200) + diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee new file mode 100644 index 0000000000..addd6e8806 --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -0,0 +1,53 @@ +async = require("async") +_ = require("lodash") +logger = require("logger-sharelatex") +DocstoreManager = require("../Docstore/DocstoreManager") +ProjectGetter = require("../Project/ProjectGetter") +ProjectUpdateHandler = require("../Project/ProjectUpdateHandler") +Project = require("../../models/Project").Project + +MILISECONDS_IN_DAY = 86400000 +module.exports = InactiveProjectManager = + + reactivateProjectIfRequired: (project_id, callback)-> + ProjectGetter.getProject project_id, {inactive:true}, (err, project)-> + if err? + logger.err err:err, project_id:project_id, "error getting project" + return callback(err) + logger.log project_id:project_id, inactive:project.inactive, "seeing if need to reactivate project" + + if !project.inactive + return callback() + + DocstoreManager.unarchiveProject project_id, (err)-> + if err? + logger.err err:err, project_id:project_id, "error reactivating project in docstore" + return callback(err) + ProjectUpdateHandler.markAsActive project_id, callback + + deactivateOldProjects: (limit, callback)-> + + sixMonthsAgo = new Date() - (MILISECONDS_IN_DAY * 1) + Project.find() + .where("lastOpened").lt(sixMonthsAgo) + .where("inactive").ne(true) + .select("_id") + .limit(limit) + .exec (err, projects)-> + if err? + logger.err err:err, "could not get projects for deactivating" + jobs = _.map projects, (project)-> + return (cb)-> + InactiveProjectManager.deactivateProject project._id, cb + logger.log numberOfProjects:projects?.length, "deactivating projects" + async.series jobs, callback + + + deactivateProject: (project_id, callback)-> + logger.log project_id:project_id, "deactivating inactive project" + DocstoreManager.archiveProject project_id, (err)-> + if err? + logger.err err:err, project_id:project_id, "error deactivating project in docstore" + return callback(err) + ProjectUpdateHandler.markAsInactive project_id, callback + diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 55b59cb76f..d447ffc547 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -14,6 +14,8 @@ _ = require("underscore") Settings = require("settings-sharelatex") SecurityManager = require("../../managers/SecurityManager") fs = require "fs" +InactiveProjectManager = require("../InactiveData/InactiveProjectManager") +ProjectUpdateHandler = require("./ProjectUpdateHandler") module.exports = ProjectController = @@ -189,6 +191,10 @@ module.exports = ProjectController = if user_id == 'openUser' return cb() SubscriptionLocator.getUsersSubscription user_id, cb + activate: (cb)-> + InactiveProjectManager.reactivateProjectIfRequired project_id, cb + markOpened: (cb)-> + ProjectUpdateHandler.markOpened project_id, cb }, (err, results)-> if err? logger.err err:err, "error getting details for project page" diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index ffabf60800..25466ec898 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -1,5 +1,6 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') +Project = require("../../models/Project").Project module.exports = markAsUpdated : (project_id, callback)-> @@ -8,3 +9,24 @@ module.exports = Project.update conditions, update, {}, (err)-> if callback? callback() + + markAsOpened : (project_id, callback)-> + conditions = {_id:project_id} + update = {lastOpened:Date.now()} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + markAsInactive: (project_id, callback)-> + conditions = {_id:project_id} + update = {inactive:true} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + markAsActive: (project_id, callback)-> + conditions = {_id:project_id} + update = { $unset: { inactive: true }} + Project.update conditions, update, {}, (err)-> + if callback? + callback() \ No newline at end of file diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index a658b42080..010d48d9dd 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -17,6 +17,8 @@ DeletedDocSchema = new Schema ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} + lastOpened : {type:Date} + inactive : { type: Boolean } owner_ref : {type:ObjectId, ref:'User'} collaberator_refs : [ type:ObjectId, ref:'User' ] readOnly_refs : [ type:ObjectId, ref:'User' ] diff --git a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee index c9eda7d1df..f32d48fdc1 100644 --- a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee @@ -13,7 +13,7 @@ describe "DocstoreManager", -> apis: docstore: url: "docstore.sharelatex.com" - "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()} + "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub(), err:->} @requestDefaults.calledWith(jar: false).should.equal true @@ -179,3 +179,42 @@ describe "DocstoreManager", -> project_id: @project_id }, "error getting all docs from docstore") .should.equal true + + + describe "archiveProject", -> + describe "with a successful response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) + @DocstoreManager.archiveProject @project_id, @callback + + it "should call the callback", -> + @callback.called.should.equal true + + describe "with a failed response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) + @DocstoreManager.archiveProject @project_id, @callback + + it "should call the callback with an error", -> + @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true + + + + describe "unarchiveProject", -> + describe "with a successful response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204) + @DocstoreManager.unarchiveProject @project_id, @callback + + it "should call the callback", -> + @callback.called.should.equal true + + describe "with a failed response code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500) + @DocstoreManager.unarchiveProject @project_id, @callback + + it "should call the callback with an error", -> + @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true + + diff --git a/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee new file mode 100644 index 0000000000..96cb4b024c --- /dev/null +++ b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee @@ -0,0 +1,85 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/InactiveData/InactiveProjectManager" +expect = require("chai").expect + +describe "InactiveProjectManager", -> + + beforeEach -> + + @settings = {} + @DocstoreManager = + unarchiveProject:sinon.stub() + @ProjectUpdateHandler = + markAsActive:sinon.stub() + markAsInactive:sinon.stub() + @ProjectGetter = + getProject:sinon.stub() + @InactiveProjectManager = SandboxedModule.require modulePath, requires: + "settings-sharelatex":@settings + "logger-sharelatex": + log:-> + err:-> + "../Docstore/DocstoreManager":@DocstoreManager + "../Project/ProjectUpdateHandler":@ProjectUpdateHandler + "../Project/ProjectGetter":@ProjectGetter + + @project_id = "1234" + + describe "reactivateProjectIfRequired", -> + + beforeEach -> + @project = {inactive:true} + @ProjectGetter.getProject.callsArgWith(2, null, @project) + @ProjectUpdateHandler.markAsActive.callsArgWith(1) + + it "should call unarchiveProject", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1) + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal true + done() + + it "should not mark project as active if error with unarchinging", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1, "error") + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + err.should.equal "error" + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false + done() + + + it "should not call unarchiveProject if it is not inactive", (done)-> + delete @project.inactive + @DocstoreManager.unarchiveProject.callsArgWith(1) + @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal false + @ProjectUpdateHandler.markAsActive.calledWith(@project_id).should.equal false + done() + + + describe "deactivateProject", -> + + beforeEach -> + + it "should call unarchiveProject and markAsInactive", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1) + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)-> + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.callsArgWith(@project_id).should.equal true + done() + + it "should not call markAsInactive if there was a problem unarchiving", (done)-> + @DocstoreManager.unarchiveProject.callsArgWith(1, "errorrr") + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)-> + err.should.equal "errorrr" + @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.callsArgWith(@project_id).should.equal false + done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index c5c14c4359..1db3e50aad 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -42,6 +42,10 @@ describe "ProjectController", -> userCanAccessProject:sinon.stub() @EditorController = renameProject:sinon.stub() + @InactiveProjectManager = + reactivateProjectIfRequired:sinon.stub() + @ProjectUpdateHandler = + markOpened: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -57,6 +61,8 @@ describe "ProjectController", -> '../../models/Project': Project:@ProjectModel "../../models/User":User:@UserModel "../../managers/SecurityManager":@SecurityManager + "../InactiveData/InactiveProjectManager":@InactiveProjectManager + "./ProjectUpdateHandler":@ProjectUpdateHandler @user = _id:"!£123213kjljkl" @@ -282,6 +288,9 @@ describe "ProjectController", -> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() + @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) + @ProjectUpdateHandler.markOpened.callsArgWith(1) + it "should render the project/editor page", (done)-> @res.render = (pageName, opts)=> @@ -321,3 +330,16 @@ describe "ProjectController", -> resCode.should.equal 401 done() @ProjectController.loadEditor @req, @res + + it "should reactivateProjectIfRequired", (done)-> + @res.render = (pageName, opts)=> + @InactiveProjectManager.reactivateProjectIfRequired.calledWith(@project_id).should.equal true + done() + @ProjectController.loadEditor @req, @res + + it "should mark project as opened", (done)-> + @res.render = (pageName, opts)=> + @ProjectUpdateHandler.markOpened.calledWith(@project_id).should.equal true + done() + @ProjectController.loadEditor @req, @res + diff --git a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee index 3a275456e7..9aac418578 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -3,7 +3,7 @@ chai = require('chai').should() modulePath = "../../../../app/js/Features/Project/ProjectUpdateHandler.js" SandboxedModule = require('sandboxed-module') -describe 'updating a project', -> +describe 'ProjectUpdateHandler', -> beforeEach -> @@ -22,3 +22,36 @@ describe 'updating a project', -> now = Date.now()+"" date.substring(0,5).should.equal now.substring(0,5) done() + + describe "markAsOpened", -> + + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsOpened project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + date = args[1].lastOpened+"" + now = Date.now()+"" + date.substring(0,5).should.equal now.substring(0,5) + done() + + describe "markAsInactive", -> + + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsInactive project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + args[1].inactive.should.equal true + done() + + describe "markAsActive", -> + it 'should send an update to mongo', (done)-> + project_id = "project_id" + @handler.markAsActive project_id, (err)=> + args = @ProjectModel.update.args[0] + args[0]._id.should.equal project_id + args[1]["$unset"].inactive.should.equal true + done() + + From 21a67ddab46ece5ec96188a98f19ea198bbb0fe7 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 13 Aug 2015 22:50:39 +0100 Subject: [PATCH 33/57] added deactivate old projects endpoint --- services/web/app/coffee/router.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 4d0c8279df..4ab506635e 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -35,6 +35,7 @@ WikiController = require("./Features/Wiki/WikiController") Modules = require "./infrastructure/Modules" RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear') RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') +InactiveProjectController = require("./Features/InactiveData/InactiveProjectController") logger = require("logger-sharelatex") _ = require("underscore") @@ -135,6 +136,8 @@ module.exports = class Router apiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject apiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf + apiRouter.get '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects + webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = From bec9bf5c87d8f8d2f83a9ee886044ad57ca34fbf Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 14 Aug 2015 09:42:27 +0100 Subject: [PATCH 34/57] replace lodash with underscore in this project --- .../coffee/Features/InactiveData/InactiveProjectManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee index addd6e8806..3d146692fd 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -1,5 +1,5 @@ async = require("async") -_ = require("lodash") +_ = require("underscore") logger = require("logger-sharelatex") DocstoreManager = require("../Docstore/DocstoreManager") ProjectGetter = require("../Project/ProjectGetter") From 66b87df17c01061b88849a4a0e866777290e4b7c Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 14 Aug 2015 11:26:11 +0100 Subject: [PATCH 35/57] added deactivate project endpoint --- .../InactiveData/InactiveProjectController.coffee | 12 ++++++++++++ services/web/app/coffee/router.coffee | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee index b37b43872f..802c56cbdf 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -1,11 +1,23 @@ InactiveProjectManager = require("./InactiveProjectManager") +logger = require("logger-sharelatex") + module.exports = deactivateOldProjects: (req, res)-> + logger.log "recived request to deactivate old projects" InactiveProjectManager.deactivateOldProjects 10, (err)-> if err? res.sendStatus(500) else res.sendStatus(200) + + deactivateProject: (req, res)-> + project_id = req.params.project_id + logger.log project_id:project_id, "recived request to deactivating project" + InactiveProjectManager.deactivateProject project_id, (err)-> + if err? + res.sendStatus 500 + else + res.sendStatus 200 \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 4ab506635e..22c1fbfe54 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -136,7 +136,8 @@ module.exports = class Router apiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject apiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf - apiRouter.get '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects + apiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects + apiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> From 70b825fd2ab023109349656e363e6af8ca4885d3 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 14 Aug 2015 11:27:11 +0100 Subject: [PATCH 36/57] fixed call to ProjectUpdateHandler.markAsOpened and made it async --- .../app/coffee/Features/Project/ProjectController.coffee | 6 ++++-- .../UnitTests/coffee/Project/ProjectControllerTests.coffee | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index d447ffc547..3c6264ecd6 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -193,8 +193,10 @@ module.exports = ProjectController = SubscriptionLocator.getUsersSubscription user_id, cb activate: (cb)-> InactiveProjectManager.reactivateProjectIfRequired project_id, cb - markOpened: (cb)-> - ProjectUpdateHandler.markOpened project_id, cb + markAsOpened: (cb)-> + #don't need to wait for this to complete + ProjectUpdateHandler.markAsOpened project_id, -> + cb() }, (err, results)-> if err? logger.err err:err, "error getting details for project page" diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 1db3e50aad..d95dbb97b9 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -45,7 +45,7 @@ describe "ProjectController", -> @InactiveProjectManager = reactivateProjectIfRequired:sinon.stub() @ProjectUpdateHandler = - markOpened: sinon.stub() + markAsOpened: sinon.stub() @ProjectController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings "logger-sharelatex": @@ -289,7 +289,7 @@ describe "ProjectController", -> @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub() @InactiveProjectManager.reactivateProjectIfRequired.callsArgWith(1) - @ProjectUpdateHandler.markOpened.callsArgWith(1) + @ProjectUpdateHandler.markAsOpened.callsArgWith(1) it "should render the project/editor page", (done)-> @@ -339,7 +339,7 @@ describe "ProjectController", -> it "should mark project as opened", (done)-> @res.render = (pageName, opts)=> - @ProjectUpdateHandler.markOpened.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true done() @ProjectController.loadEditor @req, @res From d3499acd7b05abaa719da04d53024c8fd5873e98 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 14 Aug 2015 14:11:53 +0100 Subject: [PATCH 37/57] pass options through stating how long ago want to archive from and limit --- .../InactiveData/InactiveProjectController.coffee | 6 ++++-- .../InactiveData/InactiveProjectManager.coffee | 13 ++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee index 802c56cbdf..5f6d9c2c2a 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -6,11 +6,13 @@ module.exports = deactivateOldProjects: (req, res)-> logger.log "recived request to deactivate old projects" - InactiveProjectManager.deactivateOldProjects 10, (err)-> + numberOfProjectsToArchive = req.query.limit + ageOfProjects = req.query.daysOld + InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)-> if err? res.sendStatus(500) else - res.sendStatus(200) + res.send(projectsDeactivated) deactivateProject: (req, res)-> diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee index 3d146692fd..3f277fa931 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -25,11 +25,11 @@ module.exports = InactiveProjectManager = return callback(err) ProjectUpdateHandler.markAsActive project_id, callback - deactivateOldProjects: (limit, callback)-> - - sixMonthsAgo = new Date() - (MILISECONDS_IN_DAY * 1) + deactivateOldProjects: (limit = 10, daysOld = 360, callback)-> + oldProjectDate = new Date() - (MILISECONDS_IN_DAY * daysOld) + logger.log oldProjectDate:oldProjectDate, limit:limit, daysOld:daysOld, "starting process of deactivating old projects" Project.find() - .where("lastOpened").lt(sixMonthsAgo) + .where("lastOpened").lt(oldProjectDate) .where("inactive").ne(true) .select("_id") .limit(limit) @@ -40,7 +40,10 @@ module.exports = InactiveProjectManager = return (cb)-> InactiveProjectManager.deactivateProject project._id, cb logger.log numberOfProjects:projects?.length, "deactivating projects" - async.series jobs, callback + async.series jobs, (err)-> + if err? + logger.err err:err, "error deactivating projects" + callback err, projects deactivateProject: (project_id, callback)-> From 6b295fde28d98a7667d1458b08f52ced86fb6f17 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 17 Aug 2015 14:23:36 +0100 Subject: [PATCH 38/57] Add shane image. --- .../web/public/img/about/shane_kilkelly.jpg | Bin 0 -> 26003 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 services/web/public/img/about/shane_kilkelly.jpg diff --git a/services/web/public/img/about/shane_kilkelly.jpg b/services/web/public/img/about/shane_kilkelly.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b353b2b6ad17685de4972ab7c01d2a40672268cd GIT binary patch literal 26003 zcmeFYcT`i|);GFo(n0Bh6s3do-ch|&TChzN*) zfOJVnLXloV51kvHbKY~__Z#<)amW3>|L$2CWdE`<=Uj8nmA%(*6;6MjP6Fpm42=x| z3JMC~7WoC7P65*fp&$pKw zNtRIn$nU@f@?QXj;@_BqjGw�o4E0eM7#U6>^JbR{wFm0}6Hbfw%-g1B8@hm1GqG zKv7;nMN?i~Q$bBgLE#L@s{sJbD*&J+>rl%pD#*Y4FRUOh|Bm9{e!ippS6kqBRR6+B zWIb|>^t)a`L4KNYa=wAGE^g3A?y{~>h+L?PpPYiMyd0pd7wYHY3U&_?dgSg2^3}OQ zM4_(;f!uVi*r=GvoB8Rxdx36+2e{u0H^1*14tCXWyP~Hnq#ddm3h{%u2e}A^LVSD! zHA8i-{B2y5jL)j&t_b}t5(L({^1$qlkUliPT}VY%SyuiESv$bZL(}r6;XifBpLDML z(@7W%CJR%Ng$8)aDQIYD$jK|pDJsg4C1e7_e1lvML|+ z(d7{|I7sITx#fSj1o8Wq+5g+v|4Y!#^{2LIIoxdcX8TA<_C6~Xk=-=@CFn{8B z3Q$G8_!s?~mAxC$r_+pk#`^lscPuRojc*%}=`sK?E}Y4p5nYIHP=KY;4WS3Nc0#n{ zWDd#)Z~`KLpo?pu-{1EB=i3?I{b$)7{hR#)Lo#O-l6yLM#HLkc=#7Iv-A^@`|4Zus z;dZ$U0r<6tp1-aXMOb#n6Wt^o#s319)-2W$Z+z!mTUAV2^B10sRPKrD~|yaF{i zQaq&ar0}H(rii3?N|8X3N|8XA&nS~A-%u7(exj_SY^Cg>9HE?~T&CQi0;m|NxTr2u z$xx|N=~G!y*-?2=1yDs(#ZzTc6;f4FHBxm_4N*-}tx)Y#pQC1{7NVA+)}p>eZAI-$ z?N1#|ok*Qc{gJwkx}AE6dWw3T`hx=w8W>G1O(snVO)X6a%?Qmb z&F;B#=eW*^pHn+`>zwU5?{g9763^wGL!A41Zs6R^xgA+K%=?v-Y=zQs(&}Gt<)3wkI(#_Ky(x0anrPrjlpm(DWqfep# zK;KB;Pd`h4$Z&x{oI!`-K7%*IV}>^jl?)vW6AW97jEq+pH5l(Qf*Ai|e8c#e@dx7+ z<31A`lN6IalLJ#QQ!-O2(^sZ(rY&Y>W-;b#%y!H{%*o7U%x%ng<~HkmO+*^R(e)Z*6XZJtl_MgtktajtSjf~&x@VcKksrr`h3p$hVx_RcP_ABP`F@m z0dgVnLivTR3kz(tY+`H%Y;J7N*oxTN*k;(N*oE2k*x;;XI9_UADc-xhFy4IL4&D_$4n8eDSH5_@Dn865 z%1ct0EH6b|D!TOZ(hk1>zY)J5|6Bex{uKc(folTZ0_g%R0*iv|g1Ukr!E`~i;F1uh z&~+gnp*KS9LhF|=UA}cW`11S9eU}ffh+na~^5hEQ3Qm|oSY6mdI9>Rg@Vbbgh?z*F zNQDSSlwMR_)Jrr=v{RHMCN5?p_CgFPwjjY4e*9n3_T4i4A+fR zj3SJ_-lDl>dh7MAF=KvXSK~6{brUs{D3f+mCewSS*{0LCrEUk@ZZxAbGcijun=ltO z_ccda02am;X%@IU;&=S-pzfZ#d*^P>-8oAI%Sg-4d+hg|@0H)%y?^6=%6+VrgjKNB zH)~dF2kSCxlFdz&^w{#+f^6&UXzi@+O6+#*4eT@QXB|`=VjV^u#T`Q( zyPSBPe4Lt{S)E;+s~^%lw0T(m@W{o&rO;*Pk@2J4N9(Tou34_jZr9w>-4@+-+*94> zJ#;)$Jr+E5JkvZEz4W{?y;eZ_plr~F_bu-NZxYx7Tnaw*vGGAb=pYXv4Zdu?VBdDA z05lXj;3w@D>o@7I>7U`h9$*?!5=ar~7+4>~K^|a!21^7#51tCq4ao`FgIU9>LobB- zhW-qb3X2Py4>t@ij-ZZkjrbOMIr4GjRMhpT_t6y5kD|ZD2**5&nf=S?uaA!z9)lnE zK9PTt`egg5&C|wb{LlV+HuK!*c||NsY(VVT3#}IgFR5RGUiQT)#l4L?iFb?dPLNN? zOgKt(P3%rmNP3fWn(UF>m!g`I|LWW;-&dor^82UR_xuT9aGLUK?LWQ5ROXj`TuK z*4x$(G?+AeYt(A2MaiN{n=UuyG;=nmv@oT%aqHq%>9+Frx1BpX_+7~EAt`?E;@-!7_5IER zn}hj7*b(i~n`6o2#uL+%iBrhw>3`!UvSeB!^lx5r)7`@**e6KnjIiiKeW1YsLifn4 zh3-N!LT4oA9|Yq6*!#bE%A>acfP4-BJeYsik(;^mkEZ^?+y2J?jbHu`Hz@zDN5usI zR7zy_`r+So&_Xf?-XYh&3IuryooVV@-WU3Bd62Pm_iztz_a%pF4RVKpd_BqW`?`Vt zUTXyTp0T%or|EyR{D0uXzy16-XAW`?CG$$MpHRQB0Fb9wkdU!&kb8iy%b70&#YJ90|3_H=itK-hi<|@sN@{87k~CEijV0yZoGc~rIlV-$#R|ZQNRELmx%I|=*C%Se@FKJF0d#6 zpCbELVE>K_3+SJ1aQttTQ?il&EdyuDr#SLP1j{J~V4|WR|Cp#Q0D8d5`@x^(<D35mG$8;*1j`Za@k3AAwZF;gjGhJA((inEu&vrL?L=3>MLXw{X-R8p`_7qH`3pCw?egehXuI?W~7QB(R8_IZ^ zs$rfNihOc(p#cQz(W&QeIj*ame4(y3{BBemM>1^Qu4>%5(lE;l1|ON(!=U}WV3au4 zP{zn{zh-%>B@gb2MWCim=uUyBWlfF(#gvgI$H_;RbrV-grY)!Y^K*=QH|Q=HSDvwQqZByllWeyFi3+mG?@^C@&5uRu-?L z#+|zCXW=r9i%yr^_3*rAh41q6>YX}qI^KD>9OPvF zaY%DrbIWw$3JzPnvNAS{fFIaOPc-nqcHnOg({}TVNcMk4D|~4Nq3npk*h8Erj^LY) zjwk2Q{r1bpk!Yi!8MSc@5tcULU%_AZ?GtzVof@=9EkRy8P_(le0V*AYE0fb(UauN5 zI0>(E0li}BKY|Sn4NBleJFj7cV`=meRgn z%&*@St8&`Oz<@v_Ey+Y&OY(tzzpty2Z!e(~w|RI9SbH0I$@3&8@2xJeTDC1F26ugz znr?{P*{&k=MiGf(#tSoziv!`2I?4m!KPPhOrvQY2IR&O(tjHYd?4f7o+pBl?wqG?G zJ&EK52k$z%PV1#W-@n}!Jh#Xu!Y$`J7K=B9+uNuj&wJUaG~-JHK3USaV=^>s(p^}S z7V4@TeQ`O}u*FTKw2T6!W@J?z-i|~-WOnX=kWfwX>F`)J?Qa z$mVY zrBk3<0M7Kw?zdMKNsfX0@Tx)&Omz`4h^HLnN#&BOvd~n&9twX;x_X^|NjNbD6XAOx zqlydO*;dYUv}$=Un7`C@eHp$Q6*6{zwx}iULC@GJ;93yRYZkw(x1Qi_9Ue(i?BIZS z!lZm&s^vhXj5vPZ^Sde4*Wt!=I4f{(^=`+nve2y#oDXgdIdp$5gM>w)++y5aB@c(% z`ZE^tD#!A(#%fOi#+v94F|A6cz$C)C271Ik*3K*5hE6n|5lkdhHTCYA_ERjuHt~BE zQ_bnA8%+zp7ikf-1ISr|Q3iY6i3CaTO`&_&5Fmmq%bM zip+MJ6GaXE2mRBX7HfxkRS~4ck^?~kvb8@xhFQ;xHP(q`uVWq7(WsUZ%@;~ENwHb) zkK49wTGpzkSD3&$7+isW&-nyW1Ire`7skG{eq7W{YTC7_+xYPpGKufIxlBN9(DZh- zDFWx z=$i%mm9l)WeH3l*e8|6VsMF<_06lYlY3WBIma(MgVA~Oejj0EP%jsqHSC2=LZdrVG zEzI4y2uvRPWA%pSs8kY*dpA!|Mw^E%19;+g)>vG= z8;pyZd%b6!w10@!s}*RubvkfTeN$a4uvLVGB`U?LYj<@bIcfjd4hp9R3Bno@vAyD3 zhk7<#_UG!R*Q3zVppiyQuP$*mI^`6otDVvAt;wm7b@&XrZ{G=NHT` zDGwT_!SVIEj3$Nm9eib z=-{Ea+_>}j{I15H={n(NZFNptrsheRKptoFXz9nBvh;E3vcrZvmiio8ccZPZ$d~)! zS(G%}$DpLj^OlW&20&HesQuk>{6r^iHc~QcvWRbBH*Iz{ARo1aYvGIl)t>CgA&&&t z^`_E{ITS+$+xVRSNXlmU_I!KAYp~gq`~DG3-nF6cn_0lskSlglLr$st#Ay6o$IQAE z_|*#P%=&Pfx}(=I@zu4&AxO}n-kk*kWecyvZ$=%9Zvx!)pX=y0?0btb+FUE8l79@U zc_NR$J<#Zqy44G{V7Kxv@9pki)v6%EQ?a!x@XD97Wlz4qMxodsiRWzg<1anVBYOf{S+y;5qOMH@dkc$t#2B$JGhHs#PVS!XHeD6 z)u<<0Ii|~|^|q6Z)lD^-j1{JDObz^@$~G62jHI%+9_1bC9xNYJmiJ-9>>PMwY=#^g zrH@Z49vy|`#M)kGTzHo@mdeVH3_hUg>@~)zVEgAIylSHCV-l|A*qs8-h$yu5gslt* zRV%**50CECN~Q1eVe{$$T!nFmJbsa%2c-nLER0Qr>d7gv2klvtKF%~B@2q|pvw$Y6 zJ8kFbS!9zt=@ihbXFVM}LH=}>pG?dB^Jj9ml{TM+Ubs%9zh^(T->TGOC%U{Rt8s+% z>mYpcZ z>fW|Z^DZ_j?--k+Qu{&C$??Z4J3Awpk%`T!57xVNuWLJo2Q&9?%40i4Qn|aI>o48t z$Zjde;xTr8eJ5LuYgy&t0de?6Xp~x0&VX}KKy=I=b7jm`!3!!na_zc$8LNlTR{e(8W`cX71PK)bq=n|F z)uJ7_7bU95!FLLO5E;JZqM-!8eRDb{2j<^Qx;OmpJmj5D&o4tQ!`#vNd3f~t&)xf+ z@PSHn_~60#K0?|%>fU6+v#`5d%m}KFTEh5qceL13^>fePSvPiS;wrrua3FJNdkUmF z#xSagTgLkj$TaFTo&sDc&RvA*6cmQuEjXd~!C-*x9scB7T|H0h4Mo%?G~MzUwj9rw z6U=HHwHDyqQHSE#a_vtBq8!^%+tq{1)}$e@?Y>=?X>sO=b#L5nTdwPu1?7QDPtFZB zBv0iErf+RM&R%|oh*&xahdClQ9l07M)=h6>APb~ra9B+KSQWDQs4}J%6yr%?!R?&_ zMjMaDJ0EHrmEeskCK9|nVo5!Jy%lf^hT^|g9hVQ@#5P1OgTB4fm;3$4DyK*a(zV&l z7A}22uo{8$)nN-#{VQ4IxXSN6Nz7BuF^(9u*|J%v8JCO26PM?Y_&N^l_g>7zth7!O z$9ty5;JU~S+FgT@8a;+Z-jUV)mgcc&rqDvhxxH}FEe;3qqsuTm^NPu0Q2sTHd*$;> zH|gwQPGSYsd86^L@F3WTObu)tSGnxERVJ3-l7$+9lW?Tb<)0@{2h?Xu-xHI=g!1Vo zb1CVT6XAhkBj^vrW7uH_VI9455|fsAk0QDBqxSrz{7#Mjhv$H$>pvLFeK8eI2fKA^ zIASBZc&+W=1YNwZVe?gcGTQc}bpOPvKpBNbpYRnl-u4JR*7hfW$_l?jC~KEdZ7 zqjoNNmi7#y<MVvyyFxhE<=~@U&F)%>vZm?p4ZM-AgEmG zvBH+D?$%*(pcv(mW}J0g$k`(y3O?I>u=aK%<%mOkn&thZ+<1ZWkg^tW1s5}28s8+i z{=%Xm*&}vIMy+FP6Sp@QaVc^Y#{F1XkIiUPasRHPcKds?p5C}JZxvp&ehS_%@JS-KN`9Oj#@6Y$wk5|tIN}iP z5K84R+J7!KY1d>bUg&*hF zefbkz|7rtwS2TnPsRZjYYS?GW5PF0gb;-^GW(`vex}ls=r71#eDpj| zN}5!JrB_eGpNai<%t24x1G#O@00p)1^Cg#GC;ee86NzhY!lO zLrQewtOp2=83)Y~hGXiBo}o+@D3c~PY?ZmO+K`!`g&BNwUX8pOOh0&{al|`J{510< z3ihoI77ow;OLXfK8}7r1uz=IP?yt;#(P2j3lx#60){8-vxhQri&Z(p7bXQqiT7>s@ zU6&6LCh0d{&+uS^8B`iJ^sJPdKbiw;Jl>4X=z)yn=v|DNe5!^d(37VKWxRyPP+11M z$a{MiH}7KYS3>V4$A#OhEdT$*2J>uT|8bpFZ(C2CoFwP z4fi2A>!{fKM)SQ)YT+TuSdNs!P~(f9H6h5r!xX^vT_^&))wHzp7uUl$hv7vCZ$l!%r*3l2HiFy0z~`o`ezzy;~Sz@|cLw>mf>H zJoJCMA5*Zk{az%a!F~(5b8yg1zT@kTT4qj%PmnHef^oSUTMz##N}O+4Q%HUWyNs5B z_JbXL4#F3GbDNO^f)0}fY0=KdRmXChk=T$=y;sT&`-NYXzfM~F>HqBh9mhAjbRSav zfFIInX|R?oSd84F4y170hs8wI>_jjbZy$kvf3z3V+9p`56-mEmyw3Q|iG5gEBU%(3 z!+vCssLYeCEwm^L4QVr27ae^v)|m3VcYG9%kah0(lr1`YoXbUjRqb}<{)M|LMb93T zOer5{+d#oJ+crb+9P5#gF9We17L3*^ivbOtW|l)araLzFcHO=B`Av&=Cj{r3b8jPJ z!u$(+%dZY8^@;qFn~i&y@+XXITG|VqAjEguUp2s3C>WuC?D(}$YH)Udc-L_pg0Nqn zU+j+94lPwJnGMx2JPa8BblBYv z#}A0$-hIA}C(c;uj&n<_9DDe!uP1`(HPs7#;33mXUO#WA!2F@dc@H0pie}$Jtiji< z4BB57wjk)QRu#yJireDseA(im7B6z_eRfg^U-s8PM!Fs4sTFad;bywtPGbe;O-uAL zs)LTfRaM%{Q~fI)MazxZJ=4zdjaaR|B$@sf!tooknR=45pt+pe!=_+=M|CaX2QyX$Gh=D#%?WltIowu@axGiB97|H0Y8p2~$YXH9 zS0eQu>l7mK$bB8Qo4EJ5^vGbn;6<pFQfZq(AktTyw*n@@q}AsZTP70GG&-|-W} zGyNA0O1XRBBk(~|<4~5TyYNoYE(4k0=7tt^>ai;G^Ub^+Ks?I%rpwekr0pDB8k*gC ze}9`L{a`qF=&X+lMl=hT5-d28*(!`dRveDtrt7)WYUW zv;~jYk!^K;WHO}7jrYpVe+`?Gj&j|7YQ}9f6wIh|qi?oo&$irKHCoU_wTXnJ-(2Sz(v6$WTCJ_?>m%$#hqrLzeRJtfb`>%+aKO7S5#eQgx#`J83AcixC&3v zfphK*_7vAuM-$ERFv$E*@z!5)6PMg7{pc4Tch=pSr=IoS3X)cDw!`T-BTTX6_1};m zCuqd-rU6%QSHJGO@HU;1#m0=teKpZye0~UdI?+$0IvqJFU)gyc_3YLs*4qLs;UU^H z>e`8kuYVzC*_v^wHg(PSR*^;(;GCHIQBT9e;Lw3ET?88Xb&q_PzlTRg4(UB2Tw0E) zo-38jt6XrBSQjYX5W#vQ!#Q{6TLo9+KDCHm2O$5kowjE0j$(}~ zl-I_OnPS?6LuYm^_MWfvO%q$k$?Wsca1SmXd&0G>t5m(7R9pJO4ee+*)@~icdLrp{ zz#(ssTXa$%V($f|TpMbgTZ4Vx!E^Mjv=EnSduZ(gfnd=ba!FH|oz ziAx@mVA0rhsD`!3xn#*65HE5wyq_ZRnH8U%dFCR{*-UVzq#TQK`W_Zb`5`adBAcPc z!PN_Apd(i}fDMKb%vih70GSGEMnX1}rdi&WFP@04p8}7yHGn$P!x(nw+g?* zoiq|92hC;g_~n*L>p(nasE_c?GH%g5d$cF?5$ic^A!?c77AZ}(GcgzT5)6lHAg(KF z0x{3F6ZFn6Rxf6$YFgDUFIg^RL-KQAB6XV+;4I{$P*;?fTkbRG+>ZH-<D-_PIi z&vixl$}VU3NV-3*6S&DI{J>1Jlynq)`}JhObRey@~oX_e>W_x{XdH9KcVx$fwD z_Ms8&i0&;s%lX@(tfCn}dcAuQ>7l>d-qwfczUq;78MwNRl z=~XUIt!l1Kc_f%O4VTtz`M6|oEg4$av%j$_&YZf;+f*ysw~O+B3Wm+Q7uFI{+H3ri z6V+PNxq}x=xYTp0U2o(*oVDbbv1@V#S&W?ARf z9W)5igs-kOO!dMn3RYTzH2Yu3(G<5=);TY{B>8ng{l1O6{`F&Ry^YD|vYFyfhcER| zkhE0#l1ci?r`&gGb5&{42dPnMjmD1ETm9_nH!iVvgf?Y9E7s<}t;((Vb*rnllR5F( zhmVb^@xh!7pUcdQTReG+C5=lZk3m;GnIES;x%P)C|3$&=Q{Z;uRPzH=&>^Z>dl&5< zQPG^zFLMGuIH~>Br?3e-*B910qx;+c6c{Bf8<#mABF7?XV;Whv4lB)@$0lM+{3DYi4VtSdmWVz3f#GB%*2XfGLCA9Bh9-50!5|Ejn?Q= z{6gb#QZnMh(yooBM@8{c=4S#ZyX0*Qqa6X%n3VK26~EbifU%XST%ApNpO3d?yZMkH z%a$Tt6>tiOFE>Vm9idgqv)0vP^B#kTS~Y3p&h$CjB;UDjT4_pKV`5t!&41D|^ zHwuaTk+hn8MbDVXbiv6-fmy4xA~T`acRT7pE1bMh=%BcEtM+Hj7%queiQ3-Y_YKmc zjplpcEub;5>WZv;GTObX_0|&{$~kzKUF^piwS@>on{VpDsE=aT+-*edMXAaw@LOHR zNJiWTOv0Gq2x)5&JN|pOim5JFi8mO+v50G^Ni*IyCuSg{Np?=++gX!`(7jtN98PUNYDc^hP_L5=V*&4f9 z7|Qt%dea^rZ1!ER#GD6Yq85a^nOnpyQZh}rTHHLrIqZy*I8rkqrSs~wty1s#ng(rf z@b)$r$FXCUxp{My`T^^^)O~O^e5Ww;+iZ#P)}P6J9m_oC{?RLNU;J*qqjJd5wrAg* zhP!N-iUlrh&RBdD<8;t0Xn|finva( zm;J1eNsWhQAkD3cU`bW%(oqxKa}k}l;(xEF?0fVQG82yNwYMRKAYy z)nf<@Y>#hVX{`R$WKT1!p!3dgN{snp;%dg_#0wmjYFDCA8BbpEe`TWqbk}w`X~B-; zI`AUQ63Kd{02+o1*syXUYMS)_nXt*&j!Fv;j!;^Un92IC_foGKxeDEI{qA&WC{k}4 z0fd%dr}EIr+S1d^vV&o3Dn;*|AAnL9NYU3uuC6^AVp|$CR$i>TSo&a1=0m#JkaF66 zKYwd)g^$_(NuOpfI-=nA;UIo4gt73*Omkx825&{K|H^ z$29}ei-;0r3({x?LL3|HJ_V9*#&%;B1^Zu=N{7tmSo9uQ<3gwUGmO0Uy~YaY85&ac%-^l3%(j|5qG5J4hOY}{esISirJ&`)Y=<0 z9Cc3yHC**P8g4!=>DIFQ*8Rv?s!||m0wrGBxHduzX&$brg$GASYpKa0-X}bd)E3p~ z=xQ}FjZm#M#@cHY|4fI=gqS#aPW5vw+i@qa9r4X*dBmP*eSD_IE&ITjx+-Jqea4L! zGy%dsYiYi^)+{}-$gvIdOyP0Jew@vRoK)+Oy7(@g+VSuuBBo2Q49#2%wvUqTRw^^N zJ6k1tJAvbY2lXqfw{g1mzAS20e*o}ETQLuz5r?x|+qFY)Zdy0ACYCW|6Cse^qAJHB zD0qQn40<17H4Itn?f4VWcGaXiPEr2GD}|Y#42iI-@#k*vqh104*j_^gwzXKhYZ`&I z>VXuv{wT8z=++X;ZeGKUWF|RBsW~3VLVw~|Tk*_eEqW&d&gX-{*URt=7a}!(1@(ZJ zy-E^{>&?{e3`12I-Lq+L)?+f?Ij7*2*&MWfd@bors1AZwB)X$>(>CjH1!aqVBH+W@ z{o^Kgt)zdIkSFC{RvOJrfF z<0_|y!FQ9wuQzyX?>E*!q|)al#*b7<`7i2}U8x?E%q=1X%PnsXJzuP&!GE$ z_35wUZg&OFYab|T5WUGShNzRnv;$YOkIIJ4R0MK%263u`cxV6JJ5HNpMKM}CJ1t1G zsok1k#`CtT8%1mcd7h>4Z9t%OXbwZC{q%YFNd;6KVSfb2c@I>e&69#~!J9)d7S@ra zeS0@5 zm#$jjmE6b_$g=I;Y-9zhF(lmSz2)aV+3{&X1TCB5L6-RMg9oNj)i+%5A6^=#K9I4R^_rBC z>X`)&u3EF1_xYRYaAsgPkt$h^(BTEt4-GI>$t z7x`G3(aehu))xYbO;o1ngquSU6~-hR%h9}Wa1e{{?wMN+_DyLVTpH6&yZU`DEHX?=>Q%h4legKp{eI&zcEoU^>R5Zd zVD-Wz0TXDPzbOnU37u(^iQGkMsHSp%=8Z7heUT)x^=S^3|G>#Y1S_&ob){|I5I#M< zZfwvkxfqf?)+=7vSew~A1~$UBZg6^MG=E zFMLEvup619SQ~dZalUADjaZ8uxiq3c|J{;ciiN1c_G-|eaQtraixrA>J;Mzgv9ll{ zU#~i)J!A|mSgXU>%EjJ;yqRUZ7d(~1ykLp<4v*yFEyf|@>}~46X6E38TVlx9A8JyK z#F|YWXa@6ZR+r4H8%Q9kOPkgZ9!FS^v~u2Bv~!++NpO}af7)zl$J64Mz7NWWuVgBE z7wcNMGZ67b7q1(e-*50nVU+BOb_JAY_zZO&0LnyHfXR)CG4|hnVo7E%H^%}0p6Bmv)IyK;~%(59xq{Bw9+oe(4 z_L24^i(6RVa(pJK(90y(Rd?onzcR+LWd}yuIYBh2IYIjd$7^zUIAs>@DU;X%0as#iCKNZ=C}I4C@t=3oR~vCC&?!Jggf-Vx z2Q2mYo3bV(*qh7dXO7era|VOGYr{#6wJCp|?!H~`B_$nMkSE7xL+;3-KaKIza|aQ~ zmx@O2ye5?PnOko&HE#4uD}BA#^yaCSrgf3aX7}e;9>IdmQtQkuNk((6MEsu^%5WK~RYjUYWapj0Y)H0Nz|;$;J-I~C+`fmfxbUoumh<9~ zCK&51V*{}s)l=(m@qGSMa`YKG7$zz6slW_(;%&X}aFTJvSmy1&2Q#Tnv5%Kw!C?A~ zVkP=>C^28DyCS0LTtwUFB;Id6Z@t8B4-eO--A2E`eJ^Duq3g(M z2KFONze#hg%>UsaZH)2*k;ergb2~*WH;WLclJe1o1Mz4QzN^Ak;zTa4azj1-cbxa~ z{lJ!#3E1qi*fyl1W)Mui(>}hx4O`N5)!F;A(Ym|%M9EH~Ztz&|xWH_C9MhANX<@cw zA7pp%p$0|79CCBXYY1&EAG26c@ht8?sx_8D_w=AvPXX45Za(WOn>q~skYMCi&|5k2 z$wK55IGE`=A|;+U=Syx9ob301=d>hYhffEOI~#rex=x+Q#ke^Y5Ke5p95PRJHL(Ah z^rp>#j7@PJ4BO?Ey}vX-GM0sh$Fv$_9f{b=!O`_fk>Lyesh1v7J+arON>;qi19-k3 zqKx-P4G}6Uufy(k7v-p@*%}BI`EsE5gH(D$_)k zrF`xUn)KI22}y$XpK~l)?%Nop^v6rK4EU_TXYtpm2abMFZkO8_`@$j6ttCRg)7a?1 zOooN0s07MH(K5L?KL<>5ju^*ut~(z(MM~|xK2$jwCaBh*oBe8KLT1r|= zwyxglT|eNv*#1^L9k2fY!a(syI$GPAq-qP>3mhyJ7>t7FiMe$$7Yc8QYtES)HX!hO} z((c8KZ*yh#+nJ-)buBwje>URqS;KWL-L&vnc;ssP>wmGTfGv*TSP9e5UXEW$q;#CwuW1D^r3?0pzf^RyIV~*_ghz1 zdi3b8bLKG_ns}age(^7ftyeYnw=%u7zOQd4ZTLsxx5mHPXg`7e5(qp2;5(Z;y#nLL z_m=N>b$evjpJ&xu!P;^1EiG*g(Q9PaC21rw$BRp89vDh+RHa7;7b=%>PVv`!Et1mr z?(|)6dmZ?B;xY1tIxyBrC1|T|wYHv`tzTx(>Hh%!3El96!v6rXrI&!d8hCcZ>Gr9j zYmr~gb30yY8qU9{+MPpF)^!`{w%lId>i5=fb8|e#M`;SeFp;L;4-JfXjLIrkwAAUY zT@qZa#Vs|q`}AAi-TEB=04e&H4bID!bSq)2)@feybrdAt{_6INeYz#8KVa1Z=Ld#S z_&Ie!#&KUy6=kEfn!cTTz4h&{t5&)BOr+a#ZGGMU06lHn^_8fU4&5+4J9Iq= zBh>!5`L&MQTeh1oTkCr+pL+Oll&}8)52u!vR@2wlLgM2joa3P#!*<3!K>q-1>)NR% z_Vn9s-LzlRdyw6}{l7)O=l7Cq&pk8S9Y68q{z9}#C1)3Pex84ya(UzD_x}L)kMsMF z|J41$x`!+<23sc}@;3I!$0XqL2+why)%h*1-SzpeT{P44)9qgI`(*Fc{{WYx>(iuN z{ZV5>z55-=2MP1zCnF=@-MXBq9f$YVdNWX(zRyofbX~O5&u*r5mC|omJDT^XwNeXI{Z`uTb=9qOx6al)TGvy375w$L zo}I0~-Koude*Lh%4gSx1Z-=xWiW;Kdc(3AF)pTC~{5R6H*t|#L?-sN=Hlcs1>hIyR zJa4A>4@9xId#@CD*IIio4QZC8EYkM#R=%*(?xPzjcsxB^RcR>Grxxzg_opo(PAcy5 za<-m#lW8NXEW%AD2zdeA?=v0D^ zF*r|}-0_X!B>Kr{$)$ZR(fVF9g)x|EspYbbT22kRbC)FI zaWBwA>NWW%97Y~dgXvW%dbKWB)3@rS^LVOR))w)pg2vJIso8r8&ut#G`J$fRZ?9XQ zpq~x@0BG-s8g_+l{{RmD8Qx9f9cx0e(%ZydAh3&0@PC7JsP2;9>J2*D?^Cz&4!`0Z zR9YMB%lK4AUTb^ds6n^vF?v$O;Hj&+wEqBhuF{lUyoT4O z%&`?I^U`iIdfhb`D{AL0-TPYC-skiI`z?G5@V~=v4%=Nmho{(297z?F7BLia0H-(snRJ(>#;E$73@pn5ORECG=P6>vz{zuDd%~AFgEFCr>Se zUnD)U(c5UZ)jyS%i%ofz(f5XfeS2xDSnD<|C6(5lb7^&HDHwPqfU8EwA1DEo#^_5c zt2#GVX7b@)bn3#bIW;;`jH2&*HMf)6+b3-_x@Tr%meRywu{c`MQ>BQfDpez>}Ibof4!}J z9ES}0tXwTWy%r=IARG54h9D9{{UwtEmKca)Rmfjn$fE@v-!5$ zXU->mmA8Ez*L809dfV2;$lE*)&0)AE9^6j5ylBU z7w(dI+yHM-wNzyjij-I#X8h>`tGl@B&LPoYec*8M!hRz3H_C$+$%9vGA9AGqlLuRcA-Y4 zYi`suP4}0=uX{VnHs5b_&ZTHYK6ynhs!sZJvPn0umi^D?clNmd0E3CVS952m_!ssf z@a^w~8t`0Lug5Ql+FhK!AuzMFBUA7X!>fk7u+c`_nN`Hv$5f`I(F zWx}1k&3;o#QdW+x?Rk3Xx^K7Nx%>4bnoipN_Fi9yuiSm1`#$_8@UFdMZzb-P2ok``*7L zWu~`BeJNn%^-+uMC?vJ&wB2;x-f3L)jUVBa{*!fW5wfz0r70RPQ!*$*SUSLs6}($m z0Pg%gRYpl+SUzYkdrr;jpIt4tP2XOJqlb7)`;8>@c8#}t+f?7J{%tep-2+KoD$WR` zP}e}q8%RE7+U&R_0wX|Mj3^#hW4W0|+J#b1>2#A@?S9_s-&-qu(cL)4qW4#qn_Kf+ z?5#F^&i?>T5u!=vOhRth80}OpavUmvrzmsgGi>=jZZo>Nsl_yt-`>6TSMIj;cV1dp z@GNIsrWw6_6*cu)~+qT;UXUt{u^o@2T2`L^)>A~ zRJxLR>^^ZMQbD9id|Fr?WsWI+@&k(cz7?l8n^1ATDpU5B*6`$$l%2V3lecYcZR~zc z$IzuuB*lA270l`TIW?-P>c^EwXUvkC*=a9{>&^Ko{{RHh{{VxFziA)X<44oJ75>l` z8u#s6@G8epTOA~Llf|>A#=nM|G!E9n+rgeZ*OK}l80autd2-r#KjH<+`z(+o8ZV1< zOF3TBzfr4$gyO9fts6(rUTZIP^0%({jCq@JZJ&WT>h zMG*8E%jr$6AjKeiXfIW9c&xdNa-ZX1$75@NK)R@iK*na1Fo6p~KiL zI#Z~nUUpi|r3Cu8!6vu5v)cNnbI_@nR;b#Kxfbp2?{3%A?@R8rkHwD?__M_y5xhlz z<1ZX|gI@7ouW<4`=ZL&btZI7ZyQ)o{#cnliR_5ydSVJ*qiS9&7K2$|7^7?e?LJ{SK zH3+Y>6=c0qvgzCIxy>oI)0=W{t9`w1_{m$l(B{%W9!tqK?{l^x#u7XoiaEmW0~<$f z2GB@k<7cC_?<-p0JL_w0^VZrSzMro}ZBkxuU3RZ2_G(dalv9gVlwZ817Odkf*Swvcjd?G! zp;^+LXHCi#DalR~gKANnWq8Ui*G)Zc+j@HP{_=jxpYTy%0{GWS(mWIKyIi#JH;yfm z-pUUW>WnSC9paywIH9;YyKjTTdc>}|ys_1cT3phz&t<4YjrKJB&pqJ^)T&PtmQ$7@ zgS_QN4)v*jhBi*5BDGwcvPoH|?z8re8RN_~cv8b>6=Q|PB%rI!TMu4e3}olb)fu~| zB`KPTkb#Vl< z#?6v2!9{$0DO9B=8OBkPZ<-Q}l%pi<(~OdFcUto`Z|>avDmcn>N~9+lMo>*o4N5I^ zagvjZPr8=6YP3I0tZt)<%gvh3Q5%TXQ4vMMKF}F~`8Rot1f>VgfPuGZuR&9CSF?Wa zN57Ve{{V-}baYdyo3eVXb+YqKZ+71APM5i$x9H3TwYKJk*&Iw(G~5FmgD(X#1EQz} zm;=V=+s{)BoP3u0-+s-jSv~&%J(=4I>K41UzMA=MqPo|wXL}cC*CT?^Bv2wrHr>qR zr_HkxbWaO~JK6^bp z?s#9ukJ=mIr|ka#;GIX}PsU$}ULNo-g><+X&F_eBqP3Fcz|Yw9Egc)}+TNu$7AtEl zJ`3xsnf7ODt26pIJPsojHBS#lRVp{|!Ck9p+uq47Wp~H;sgE)hyMTtEY!blUx?adUK+L4yg%?a;px1O!5S{8 zLTer&)aNY~-;Df0YK-yNTwK7uUYVqR_4Ml&b9lz`ca9I$vK*G4Q%%mJjPX@sl}RmQ zPCL4AX*VmobI$8qB$|t7^CuW_O->-hsx&$2W%y~%RAVo8Sc=`ekli8@}2t6}oh_*PHY{FHbxA z>81Yw&+omq4J*VtpNI8JEn7^!ywf!;F6tdZ!%@@jXR_3E%lMc)H~MA$tgzl`_mH}R zw{~;EaD*q9A`lA0NvCzb{{Sxk04+LN{cOtu z7V*Oi+{&>lR!^AL>bs}?fffRw&v}$S8Z3xd#~ktd9s(fum1pU+25tUU2Wg-lytAk zMFe)x`l8kJ(Z(l#o-+$kBE9-u@zkl-nx;>1k z;|P$%PSQ)r{kMeZ2Mf27Gqp|u$2lzA(o3wG_1op;e?PlJRNJ=cU3}HLq~GbSv^8E3 z`h0mfD2mE^hRAKig~<2Dc*b$jx}zJxC8nP@%d>ZS@6q?w=wjcz)AjTFwer!uHrCCV zixIfQQXJHy6E_ELP_=iTYd4WLB zI%tYq?%$ooYs;&``Bc~L;aZ0^nozA7!YSVAMx-Sr)oVXDeK~%+2jUichfwy<3yS5^ z^=UeCeJF42`JAy6zx}%E zMOint@2gf$UoiYd{{Vv7{{X>5z9d037XJViG!KUM_Ys(xJ{Wi#N6|0DSqExs+S~Y( zS3%JEk=(&BvDLf;(v~OUE}W%OwPwy@sEi0PZ4SW zvoU|OOR3%5t(;BT8#K0r50PTwLi*IO)M!CMg&I^6R^_KoG3fMso40#wch%~WkH}+l z3|?VcRq?o-MlQ6UGpUHJQmn7#%=uNjMltExGl0|vbot0_vPv2?$OVcWgKG33uI_M0 z86!CP_HlN1>-S#WHoDUKzqsRi^>5euf1loU*3v8JaIh>I-O(GXoyvt&l1KqnN}rTq z@s$J`+A(~V(^q@h?bGcpz4?6aptt1!0<=TXjfs2%WI2Wh}Of$9m)KtFj( z!&XT(-ul}9o7&s;{{XkGVEny%{=e|WJV-bIk?Xj3>N)3w$2t0*wJkgD*Or=n@PF5B zho^VShB}VBaR>5~Xed3_t50`*AP#3Z=jAQA3yV>>ix9QMr z-}?Tx^j|%r&rkc?)sOCl9$@N7+`F(ca0tl(xjn!bu6CTH)%N-J^N-(uyX_q>=D!>N z0GDsLsi=UrC^3++uNfd?q3GG?uU6-`6_a+is$Dl7l)9uyhy7br7ApuVr=Nb92o~zTGbmxEiAd;H%h$IN#0=s;I(Bd_y6OMTnhyYwP!mdkf`uKRyaQ8F`z8DMg7RP);>cYc`_r8ve>cfY%Rp8j54HNQ@l1*{(WUv9plPb<0Eov9pX z0*>G|Hj~r=+Xs()3W*?jueal}h39pe_-4%v+2EpKM z2u~QtJBpl Date: Tue, 18 Aug 2015 14:21:35 +0100 Subject: [PATCH 39/57] Move the template-browser code into the 'templates' module. Now located to be located at `modules/templates/public/coffee/main/index.coffee`, moved to complete a migration of template related code into the templates module. Corresponding commit in sharelatex-web-modules: 625c556e42072d30f30d474aaef72deff24ca154 --- services/web/public/coffee/main.coffee | 1 - .../web/public/coffee/main/templates.coffee | 77 ------------------- 2 files changed, 78 deletions(-) delete mode 100644 services/web/public/coffee/main/templates.coffee diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index 88e33694e7..3740b679c2 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -3,7 +3,6 @@ define [ "main/user-details" "main/account-settings" "main/account-upgrade" - "main/templates" "main/plans" "main/group-members" "main/scribtex-popup" diff --git a/services/web/public/coffee/main/templates.coffee b/services/web/public/coffee/main/templates.coffee deleted file mode 100644 index b7be0bac4c..0000000000 --- a/services/web/public/coffee/main/templates.coffee +++ /dev/null @@ -1,77 +0,0 @@ -define [ - "base" -], (App) -> - - App.controller "openInSlController", ($scope) -> - - $scope.openInSlText = "Open in ShareLaTeX" - $scope.isDisabled = false - - $scope.open = -> - $scope.openInSlText = "Creating..." - $scope.isDisabled = true - ga('send', 'event', 'template-site', 'open-in-sl', $('.page-header h1').text()) - - $scope.downloadZip = -> - ga('send', 'event', 'template-site', 'download-zip', $('.page-header h1').text()) - - - App.factory "algolia", -> - if window?.sharelatex?.algolia?.app_id? - client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key) - index = client.initIndex(window.sharelatex.algolia?.indexes?.templates) - return index - - - - App.controller "SearchController", ($scope, algolia, _) -> - $scope.hits = [] - - $scope.clearSearchText = -> - $scope.searchQueryText = "" - updateHits [] - - $scope.safeApply = (fn)-> - phase = $scope.$root.$$phase - if(phase == '$apply' || phase == '$digest') - $scope.$eval(fn) - else - $scope.$apply(fn) - - buildHitViewModel = (hit)-> - result = - name : hit._highlightResult.name.value - description: hit._highlightResult.description.value - url :"/templates/#{hit._id}" - image_url: "#{window.sharelatex?.templates?.cdnDomain}/#{hit._id}/v/#{hit.version}/pdf-converted-cache/style-thumbnail" - - updateHits = (hits)-> - $scope.safeApply -> - $scope.hits = hits - - $scope.search = -> - query = $scope.searchQueryText - if !query? or query.length == 0 - updateHits [] - return - - query = "#{window.sharelatex?.templates?.user_id} #{query}" - algolia.search query, (err, response)-> - if response.hits.length == 0 - updateHits [] - else - hits = _.map response.hits, buildHitViewModel - updateHits hits - - - App.controller "MissingTemplateController", ($scope, $modal)-> - $scope.showMissingTemplateModal = -> - $modal.open { - templateUrl: "missingTemplateModal" - controller:"MissingTemplateModalController" - } - - App.controller "MissingTemplateModalController", ($scope, $modalInstance) -> - $scope.cancel = () -> - $modalInstance.dismiss() - From 865372d13fcd110dd41ac09bd2c86a156c763dc2 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 18 Aug 2015 14:45:44 +0100 Subject: [PATCH 40/57] Add some whitespace around buttons in template-details. --- services/web/public/stylesheets/app/templates.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/stylesheets/app/templates.less b/services/web/public/stylesheets/app/templates.less index a08f5b8d3c..ac6f641f32 100644 --- a/services/web/public/stylesheets/app/templates.less +++ b/services/web/public/stylesheets/app/templates.less @@ -49,6 +49,9 @@ .template-details-section { padding-bottom: 20px; + a.btn { + margin-left: 6px; + } } .searchResult { From 18f75bba794d22b11ebcb90e0f226a3974f53af1 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 19 Aug 2015 09:59:01 +0100 Subject: [PATCH 41/57] Generalize this style to all .btn elements under the .template-details-section --- services/web/public/stylesheets/app/templates.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/templates.less b/services/web/public/stylesheets/app/templates.less index ac6f641f32..f7d490ff48 100644 --- a/services/web/public/stylesheets/app/templates.less +++ b/services/web/public/stylesheets/app/templates.less @@ -49,7 +49,7 @@ .template-details-section { padding-bottom: 20px; - a.btn { + .btn { margin-left: 6px; } } From 50fc886c945a501ac1d4225f5e0ee3e86d3fc9b6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 19 Aug 2015 11:54:30 +0100 Subject: [PATCH 42/57] changed inactive to active as its more effienct query in mongo --- .../Features/InactiveData/InactiveProjectManager.coffee | 8 ++++---- .../coffee/Features/Project/ProjectUpdateHandler.coffee | 4 ++-- services/web/app/coffee/models/Project.coffee | 2 +- .../InactiveData/InactiveProjectManagerTests.coffee | 6 +++--- .../coffee/Project/ProjectUpdateHandlerTests.coffee | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee index 3f277fa931..7885107c92 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -10,13 +10,13 @@ MILISECONDS_IN_DAY = 86400000 module.exports = InactiveProjectManager = reactivateProjectIfRequired: (project_id, callback)-> - ProjectGetter.getProject project_id, {inactive:true}, (err, project)-> + ProjectGetter.getProject project_id, {active:true}, (err, project)-> if err? logger.err err:err, project_id:project_id, "error getting project" return callback(err) - logger.log project_id:project_id, inactive:project.inactive, "seeing if need to reactivate project" + logger.log project_id:project_id, active:project.active, "seeing if need to reactivate project" - if !project.inactive + if project.active return callback() DocstoreManager.unarchiveProject project_id, (err)-> @@ -30,7 +30,7 @@ module.exports = InactiveProjectManager = logger.log oldProjectDate:oldProjectDate, limit:limit, daysOld:daysOld, "starting process of deactivating old projects" Project.find() .where("lastOpened").lt(oldProjectDate) - .where("inactive").ne(true) + .where("active").equals(true) .select("_id") .limit(limit) .exec (err, projects)-> diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index 25466ec898..7738425c8e 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -19,14 +19,14 @@ module.exports = markAsInactive: (project_id, callback)-> conditions = {_id:project_id} - update = {inactive:true} + update = {active:false} Project.update conditions, update, {}, (err)-> if callback? callback() markAsActive: (project_id, callback)-> conditions = {_id:project_id} - update = { $unset: { inactive: true }} + update = {active:true} Project.update conditions, update, {}, (err)-> if callback? callback() \ No newline at end of file diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 010d48d9dd..8c458e28ed 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -18,7 +18,7 @@ ProjectSchema = new Schema name : {type:String, default:'new project'} lastUpdated : {type:Date, default: () -> new Date()} lastOpened : {type:Date} - inactive : { type: Boolean } + active : { type: Boolean, default: true } owner_ref : {type:ObjectId, ref:'User'} collaberator_refs : [ type:ObjectId, ref:'User' ] readOnly_refs : [ type:ObjectId, ref:'User' ] diff --git a/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee index bd538c37f3..6befd7f70f 100644 --- a/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee @@ -33,7 +33,7 @@ describe "InactiveProjectManager", -> describe "reactivateProjectIfRequired", -> beforeEach -> - @project = {inactive:true} + @project = {active:false} @ProjectGetter.getProject.callsArgWith(2, null, @project) @ProjectUpdateHandler.markAsActive.callsArgWith(1) @@ -53,8 +53,8 @@ describe "InactiveProjectManager", -> done() - it "should not call unarchiveProject if it is not inactive", (done)-> - delete @project.inactive + it "should not call unarchiveProject if it is active", (done)-> + @project.active = true @DocstoreManager.unarchiveProject.callsArgWith(1) @InactiveProjectManager.reactivateProjectIfRequired @project_id, (err)=> @DocstoreManager.unarchiveProject.calledWith(@project_id).should.equal false diff --git a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee index 9aac418578..5922a027e3 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -42,7 +42,7 @@ describe 'ProjectUpdateHandler', -> @handler.markAsInactive project_id, (err)=> args = @ProjectModel.update.args[0] args[0]._id.should.equal project_id - args[1].inactive.should.equal true + args[1].active.should.equal false done() describe "markAsActive", -> @@ -51,7 +51,7 @@ describe 'ProjectUpdateHandler', -> @handler.markAsActive project_id, (err)=> args = @ProjectModel.update.args[0] args[0]._id.should.equal project_id - args[1]["$unset"].inactive.should.equal true + args[1].active.should.equal true done() From a777fcc5a6661f1c152b49c126fc27b684338a47 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 19 Aug 2015 11:55:35 +0100 Subject: [PATCH 43/57] changed post to deactivate projects to set params via body rather than query params --- .../Features/InactiveData/InactiveProjectController.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee index 5f6d9c2c2a..392e5e2f4e 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -6,8 +6,8 @@ module.exports = deactivateOldProjects: (req, res)-> logger.log "recived request to deactivate old projects" - numberOfProjectsToArchive = req.query.limit - ageOfProjects = req.query.daysOld + numberOfProjectsToArchive = req.body.numberOfProjectsToArchive + ageOfProjects = req.body.ageOfProjects InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)-> if err? res.sendStatus(500) From 63580f6a798fe1a12b8672fa26688e70297ed166 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 19 Aug 2015 11:58:41 +0100 Subject: [PATCH 44/57] remove useClsi2 flag in project collection --- .../app/coffee/Features/Project/ProjectCreationHandler.coffee | 1 - services/web/app/coffee/models/Project.coffee | 1 - .../test/UnitTests/coffee/Compile/CompileControllerTests.coffee | 1 - 3 files changed, 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index 6871f8e6d2..c461c04341 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -19,7 +19,6 @@ module.exports = project = new Project owner_ref : new ObjectId(owner_id) name : projectName - useClsi2 : true project.rootFolder[0] = rootFolder User.findById owner_id, "ace.spellCheckLanguage", (err, user)-> project.spellCheckLanguage = user.ace.spellCheckLanguage diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 8c458e28ed..111ef40cfb 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -28,7 +28,6 @@ ProjectSchema = new Schema compiler : {type:String, default:'pdflatex'} spellCheckLanguage : {type:String, default:'en'} deletedByExternalDataSource : {type: Boolean, default: false} - useClsi2 : {type:Boolean, default: true} description : {type:String, default:''} archived : { type: Boolean } deletedDocs : [DeletedDocSchema] diff --git a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee index 08299812fe..33239f53a6 100644 --- a/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Compile/CompileControllerTests.coffee @@ -98,7 +98,6 @@ describe "CompileController", -> describe "when downloading for embedding", -> beforeEach -> - @project.useClsi2 = true @CompileController.proxyToClsi = sinon.stub() @CompileController.downloadPdf(@req, @res, @next) From 0e9ec00f8509f6cce1021c98dea3b4c7f244f396 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 19 Aug 2015 11:58:59 +0100 Subject: [PATCH 45/57] added stubbed logger in document controller tests --- .../coffee/Documents/DocumentControllerTests.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee index b8419b38cf..ee1360fa0a 100644 --- a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee @@ -11,7 +11,10 @@ Errors = require "../../../../app/js/errors" describe "DocumentController", -> beforeEach -> - @DocumentController = SandboxedModule.require modulePath, requires: + @DocumentController = SandboxedModule.require modulePath, requires: + "logger-sharelatex": + log:-> + err:-> "../Project/ProjectEntityHandler": @ProjectEntityHandler = {} @res = new MockResponse() @req = new MockRequest() From 2dd56d0b32461e390127c44a293f3b63b919a03c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 12:02:43 +0100 Subject: [PATCH 46/57] If we're sending a html file to mobile-safari, do so as plain text. This prevents safari from trying to render the page, which it does because it ignores the "Content-Disposition" header. --- .../FileStore/FileStoreController.coffee | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee index 85ca986eff..17e3fb40f4 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee @@ -1,13 +1,22 @@ logger = require('logger-sharelatex') FileStoreHandler = require("./FileStoreHandler") ProjectLocator = require("../Project/ProjectLocator") - +_ = require('underscore') + +is_mobile_safari = (user_agent) -> + user_agent and (user_agent.indexOf('iPhone') >= 0 or + user_agent.indexOf('iPad') >= 0) + +is_html = (file) -> + file.name.lastIndexOf('.html') == file.name.length - 5 + module.exports = getFile : (req, res)-> project_id = req.params.Project_id file_id = req.params.File_id queryString = req.query + user_agent = req.get('User-Agent') logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download" ProjectLocator.findElement {project_id: project_id, element_id: file_id, type: "file"}, (err, file)-> if err? @@ -17,5 +26,9 @@ module.exports = if err? logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error getting file stream for downloading file" return res.sendStatus 500 + # mobile safari will try to render html files, prevent this + if (is_mobile_safari(user_agent) and is_html(file)) + logger.log filename: file.name, user_agent: user_agent, "sending html file to mobile-safari as plain text" + res.setHeader('Content-Type', 'text/plain') res.setHeader("Content-Disposition", "attachment; filename=#{file.name}") - stream.pipe res \ No newline at end of file + stream.pipe res From ce248f56d772a10b55539994168b6a1aaed82450 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 12:28:51 +0100 Subject: [PATCH 47/57] Un-break FileStoreControllerTests by mocking out the `req.get` method. --- .../coffee/FileStore/FileStoreControllerTests.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee index fc665cee7a..0560416287 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee @@ -21,14 +21,15 @@ describe "FileStoreController", -> @stream = {} @project_id = "2k3j1lk3j21lk3j" @file_id = "12321kklj1lk3jk12" - @req = + @req = params: Project_id: @project_id File_id: @file_id query: "query string here" - @res = + get: (key) -> undefined + @res = setHeader: sinon.stub() - @file = + @file = name: "myfile.png" describe "getFile", -> From 85c1704fc5adf7343e2fb580ae1fd1f822602745 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 14:03:12 +0100 Subject: [PATCH 48/57] Test that content-type is set for mobile-safari user-agents. --- .../FileStore/FileStoreControllerTests.coffee | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee index 0560416287..6dbcd92722 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee @@ -50,7 +50,7 @@ describe "FileStoreController", -> done() @controller.getFile @req, @res - it "should get the file from the db", (done)-> + it "should get the file from the db", (done)-> @stream.pipe = (des)=> opts = project_id: @project_id @@ -65,5 +65,45 @@ describe "FileStoreController", -> @res.setHeader.calledWith("Content-Disposition", "attachment; filename=#{@file.name}").should.equal true done() @controller.getFile @req, @res - + describe "with an HTML file", -> + + beforeEach -> + @user_agent = 'A generic browser' + @file.name = 'really_bad.html' + @req.get = (key) => + if key == 'User-Agent' + @user_agent + + describe "from firefox", -> + + beforeEach -> + @user_agent = "A Firefox browser" + + it "should not set Content-Type", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false + done() + @controller.getFile @req, @res + + describe "from an iPhone", -> + + beforeEach -> + @user_agent = "An iPhone browser" + + it "should set Content-Type to 'text/plain'", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true + done() + @controller.getFile @req, @res + + describe "from an iPad", -> + + beforeEach -> + @user_agent = "An iPad browser" + + it "should set Content-Type to 'text/plain'", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true + done() + @controller.getFile @req, @res From aab7a8713e5ad7d665915abc82bb72b5417cdd0c Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 15:56:30 +0100 Subject: [PATCH 49/57] Catch the case where filename is shorter than the extension length. --- .../coffee/Features/FileStore/FileStoreController.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee index 17e3fb40f4..c36ce22151 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee @@ -8,7 +8,12 @@ is_mobile_safari = (user_agent) -> user_agent.indexOf('iPad') >= 0) is_html = (file) -> - file.name.lastIndexOf('.html') == file.name.length - 5 + ends_with = (ext) -> + file.name? and + file.name.length > ext.length and + (file.name.lastIndexOf(ext) == file.name.length - ext.length) + + ends_with('.html') or ends_with('.htm') or ends_with('.xhtml') module.exports = From ebf754904f136d5aed6ea517574ab0c9816584fe Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 15:56:56 +0100 Subject: [PATCH 50/57] More thorough tests for the FileStoreController when downloading .html files. --- .../FileStore/FileStoreControllerTests.coffee | 87 ++++++++++++------- 1 file changed, 54 insertions(+), 33 deletions(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee index 6dbcd92722..1d24d11957 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee @@ -66,44 +66,65 @@ describe "FileStoreController", -> done() @controller.getFile @req, @res - describe "with an HTML file", -> - - beforeEach -> - @user_agent = 'A generic browser' - @file.name = 'really_bad.html' - @req.get = (key) => - if key == 'User-Agent' - @user_agent - - describe "from firefox", -> + # Test behaviour around handling html files + ['.html', '.htm', '.xhtml'].forEach (extension) -> + describe "with a '#{extension}' file extension", -> beforeEach -> - @user_agent = "A Firefox browser" + @user_agent = 'A generic browser' + @file.name = "bad#{extension}" + @req.get = (key) => + if key == 'User-Agent' + @user_agent - it "should not set Content-Type", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false - done() - @controller.getFile @req, @res + describe "from a non-ios browser", -> - describe "from an iPhone", -> + it "should not set Content-Type", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false + done() + @controller.getFile @req, @res + + describe "from an iPhone", -> + + beforeEach -> + @user_agent = "An iPhone browser" + + it "should set Content-Type to 'text/plain'", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true + done() + @controller.getFile @req, @res + + describe "from an iPad", -> + + beforeEach -> + @user_agent = "An iPad browser" + + it "should set Content-Type to 'text/plain'", (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true + done() + @controller.getFile @req, @res + + # None of these should trigger the iOS/html logic + ['x.html-is-rad', 'html.pdf', 'bare'].forEach (filename) -> + describe "with filename as '#{filename}'", -> beforeEach -> - @user_agent = "An iPhone browser" + @user_agent = 'A generic browser' + @file.name = filename + @req.get = (key) => + if key == 'User-Agent' + @user_agent - it "should set Content-Type to 'text/plain'", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true - done() - @controller.getFile @req, @res + ['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach (browser) -> + describe "downloaded from #{browser}", -> + beforeEach -> + @user_agent = "Some #{browser} thing" - describe "from an iPad", -> - - beforeEach -> - @user_agent = "An iPad browser" - - it "should set Content-Type to 'text/plain'", (done) -> - @stream.pipe = (des) => - @res.setHeader.calledWith("Content-Type", "text/plain").should.equal true - done() - @controller.getFile @req, @res + it 'Should not set the Content-type', (done) -> + @stream.pipe = (des) => + @res.setHeader.calledWith("Content-Type", "text/plain").should.equal false + done() + @controller.getFile @req, @res From 23bd5d317c8f255bf3ed9001374b99b1e01a5bc4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 20 Aug 2015 15:58:36 +0100 Subject: [PATCH 51/57] A few extra cases which should not be treated as html. --- .../UnitTests/coffee/FileStore/FileStoreControllerTests.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee index 1d24d11957..fd83f73379 100644 --- a/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee @@ -108,7 +108,7 @@ describe "FileStoreController", -> @controller.getFile @req, @res # None of these should trigger the iOS/html logic - ['x.html-is-rad', 'html.pdf', 'bare'].forEach (filename) -> + ['x.html-is-rad', 'html.pdf', '.html-is-good-for-hidden-files', 'somefile'].forEach (filename) -> describe "with filename as '#{filename}'", -> beforeEach -> From a53e3b80cf7fa2c490b204548393cb07b8fc7e98 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 20 Aug 2015 16:55:06 +0100 Subject: [PATCH 52/57] if blog or universities site is down don't crash, send 500 --- services/web/app/coffee/Features/Blog/BlogController.coffee | 3 ++- .../coffee/Features/StaticPages/UniversityController.coffee | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 54a716c789..186d11d348 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/services/web/app/coffee/Features/Blog/BlogController.coffee @@ -22,9 +22,10 @@ module.exports = BlogController = logger.log url:url, "proxying request to blog api" request.get blogUrl, (err, r, data)-> - return next(err) if err? if r?.statusCode == 404 return ErrorController.notFound(req, res, next) + if err? + return res.send 500 data = data.trim() try data = JSON.parse(data) diff --git a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee index d82ac2ecd0..9b55b60077 100644 --- a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee +++ b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee @@ -18,6 +18,8 @@ module.exports = UniversityController = request.get universityUrl, (err, r, data)-> if r?.statusCode == 404 return ErrorController.notFound(req, res, next) + if err? + return res.send 500 data = data.trim() try data = JSON.parse(data) From 0aaeb6671e9d4550bb01f949c51ea67415b58db5 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 24 Aug 2015 11:53:33 +0100 Subject: [PATCH 53/57] Keep password reset token in session, and strip it from reset page url. This fixes an issue where the reset token was leaked in the referrer header when navigating away from the password reset page to an external site. Now we get the token from the query string, store it in the session, then redirect to the bare url of the password reset page, which then uses the stored token to render the reset form. --- .../PasswordResetController.coffee | 16 +++++-- .../PasswordResetControllerTests.coffee | 47 ++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index 06bd4ba023..d574a6838f 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee @@ -6,12 +6,12 @@ module.exports = renderRequestResetForm: (req, res)-> logger.log "rendering request reset form" - res.render "user/passwordReset", + res.render "user/passwordReset", title:"reset_password" requestReset: (req, res)-> email = req.body.email.trim().toLowerCase() - opts = + opts = endpointName: "password_reset_rate_limit" timeInterval: 60 subjectName: req.ip @@ -28,17 +28,23 @@ module.exports = res.send 404, {message: req.i18n.translate("cant_find_email")} renderSetPasswordForm: (req, res)-> - res.render "user/setPassword", + if req.query.passwordResetToken? + req.session.resetToken = req.query.passwordResetToken + return res.redirect('/user/password/set') + if !req.session.resetToken? + return res.redirect('/user/password/reset') + res.render "user/setPassword", title:"set_password" - passwordResetToken:req.query.passwordResetToken + passwordResetToken: req.session.resetToken setNewUserPassword: (req, res)-> {passwordResetToken, password} = req.body if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 return res.sendStatus 400 + delete req.session.resetToken PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) -> return next(err) if err? if found res.sendStatus 200 else - res.send 404, {message: req.i18n.translate("password_reset_token_expired")} \ No newline at end of file + res.send 404, {message: req.i18n.translate("password_reset_token_expired")} diff --git a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee index efc4d9cbde..140bf0b9d4 100644 --- a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee @@ -14,7 +14,7 @@ describe "PasswordResetController", -> @PasswordResetHandler = generateAndEmailResetToken:sinon.stub() setNewUserPassword:sinon.stub() - @RateLimiter = + @RateLimiter = addCount: sinon.stub() @PasswordResetController = SandboxedModule.require modulePath, requires: "settings-sharelatex":@settings @@ -32,6 +32,8 @@ describe "PasswordResetController", -> password:@password i18n: translate:-> + session: {} + query: {} @res = {} @@ -86,6 +88,9 @@ describe "PasswordResetController", -> describe "setNewUserPassword", -> + beforeEach -> + @req.session.resetToken = @token + it "should tell the user handler to reset the password", (done)-> @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) @res.sendStatus = (code)=> @@ -119,5 +124,45 @@ describe "PasswordResetController", -> done() @PasswordResetController.setNewUserPassword @req, @res + it "should clear the session.resetToken", (done) -> + @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true) + @res.sendStatus = (code)=> + code.should.equal 200 + @req.session.should.not.have.property 'resetToken' + done() + @PasswordResetController.setNewUserPassword @req, @res + describe "renderSetPasswordForm", -> + describe "with token in query-string", -> + beforeEach -> + @req.query.passwordResetToken = @token + + it "should set session.resetToken and redirect", (done) -> + @req.session.should.not.have.property 'resetToken' + @res.redirect = (path) => + path.should.equal '/user/password/set' + @req.session.resetToken.should.equal @token + done() + @PasswordResetController.renderSetPasswordForm(@req, @res) + + describe "without a token in query-string", -> + + describe "with token in session", -> + beforeEach -> + @req.session.resetToken = @token + + it "should render the page, passing the reset token", (done) -> + @res.render = (template_path, options) => + options.passwordResetToken.should.equal @req.session.resetToken + done() + @PasswordResetController.renderSetPasswordForm(@req, @res) + + describe "without a token in session", -> + + it "should redirect to the reset request page", (done) -> + @res.redirect = (path) => + path.should.equal "/user/password/reset" + @req.session.should.not.have.property 'resetToken' + done() + @PasswordResetController.renderSetPasswordForm(@req, @res) From 40704b486e52320141580e3b78a33cd8aa32e527 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 28 Aug 2015 16:51:52 +0100 Subject: [PATCH 54/57] Don't lock up on very long lined documents --- .../coffee/Features/Project/ProjectRootDocManager.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee index da2743d661..a6f429dca2 100644 --- a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee +++ b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee @@ -15,8 +15,12 @@ module.exports = ProjectRootDocManager = return (cb)-> rootDocId = null for line in doc.lines || [] - match = line.match /(.*)\\documentclass/ # no lookbehind in js regexp :( - isRootDoc = Path.extname(path).match(/\.R?tex$/) and match and !match[1].match /%/ + # We've had problems with this regex locking up CPU. + # Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :() + # This regex will only look from the start of the line, including whitespace so will return quickly + # regardless of line length. + match = line.match /^\s*\\documentclass/ + isRootDoc = Path.extname(path).match(/\.R?tex$/) and match if isRootDoc rootDocId = doc?._id cb(rootDocId) From 639424f664d96b22bd21d11bcfd11ccc5e9e02a2 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 1 Sep 2015 17:25:13 +0100 Subject: [PATCH 55/57] Don't error on project clone if not root doc is set --- .../web/app/coffee/Features/Project/ProjectLocator.coffee | 5 ++++- .../UnitTests/coffee/Project/ProjectLocatorTests.coffee | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index ead0dc3942..998e2fc60b 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -54,7 +54,10 @@ module.exports = findRootDoc : (opts, callback)-> getRootDoc = (project)=> - @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback + if project.rootDoc_id? + @findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback + else + callback null, null {project, project_id} = opts if project? getRootDoc project diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index b1e2cb9139..18cdb66216 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -159,6 +159,13 @@ describe 'project model', -> assert !err? doc._id.should.equal rootDoc._id done() + + it 'should return null when the project has no rootDoc', (done) -> + project.rootDoc_id = null + @locator.findRootDoc project, (err, doc)-> + assert !err? + expect(doc).to.equal null + done() describe 'findElementByPath', -> From 34c8f22dd75cb417fa14a1c96aec79e79c1d03b9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 2 Sep 2015 12:20:37 +0100 Subject: [PATCH 56/57] Allow multiple collaborators to be added at once with a list of emails --- .../web/app/views/project/editor/share.jade | 6 ++-- .../ShareProjectModalController.coffee | 33 ++++++++++++++----- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 83e4c52f38..4b73c414c4 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -48,8 +48,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .small #{translate("share_with_your_collabs")} .form-group input.form-control( - type="email" - placeholder="Enter email address..." + type="text" + placeholder="joe@example.com, sue@example.com, ..." ng-model="inputs.email" focus-on="open" ) @@ -64,7 +64,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') |    button.btn.btn-info( type="submit" - ng-click="addMember()" + ng-click="addMembers()" ) #{translate("share")} div.text-center(ng-hide="canAddCollaborators") p #{translate("need_to_upgrade_for_more_collabs")}. diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 225a841a6a..e569a2fee8 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -22,19 +22,34 @@ define [ allowedNoOfMembers = $scope.project.features.collaborators $scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS - $scope.addMember = () -> + $scope.addMembers = () -> return if !$scope.inputs.email? or $scope.inputs.email == "" + + emails = $scope.inputs.email.split(/,\s*/) + $scope.inputs.email = "" $scope.state.error = null $scope.state.inflight = true - projectMembers - .addMember($scope.inputs.email, $scope.inputs.privileges) - .success (data) -> + + do addNextMember = () -> + if emails.length == 0 or !$scope.canAddCollaborators $scope.state.inflight = false - $scope.inputs.email = "" - $scope.project.members.push data?.user - .error () -> - $scope.state.inflight = false - $scope.state.error = "Sorry, something went wrong :(" + $scope.$apply() + return + + email = emails.shift() + projectMembers + .addMember(email, $scope.inputs.privileges) + .success (data) -> + if data?.user # data.user is false if collaborator limit is hit. + $scope.project.members.push data.user + setTimeout () -> + # Give $scope a chance to update $scope.canAddCollaborators + # with new collaborator information. + addNextMember() + , 0 + .error () -> + $scope.state.inflight = false + $scope.state.error = "Sorry, something went wrong :(" $scope.removeMember = (member) -> From 7446572ed990396614f21ed358eadf60e1e01146 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 2 Sep 2015 14:31:52 +0100 Subject: [PATCH 57/57] Group online users into dropdown menu when more than 3 --- .../web/app/views/project/editor/header.jade | 37 ++++++++++++++----- .../online-users/OnlineUsersManager.coffee | 6 ++- .../stylesheets/app/editor/online-users.less | 27 +++++++++++++- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade index f41bd6f77c..06ac949c04 100644 --- a/services/web/app/views/project/editor/header.jade +++ b/services/web/app/views/project/editor/header.jade @@ -57,15 +57,34 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading") ng-show="onlineUsersArray.length > 0" ng-controller="OnlineUsersController" ) - span.online-user( - ng-repeat="user in onlineUsersArray", - ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", - popover="{{ user.name }}" - popover-placement="bottom" - popover-append-to-body="true" - popover-trigger="mouseenter" - ng-click="gotoUser(user)" - ) {{ user.name.slice(0,1) }} + span(ng-if="onlineUsersArray.length < 4") + span.online-user( + ng-repeat="user in onlineUsersArray", + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", + popover="{{ user.name }}" + popover-placement="bottom" + popover-append-to-body="true" + popover-trigger="mouseenter" + ng-click="gotoUser(user)" + ) {{ user.name.slice(0,1) }} + + span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") + span.online-user.online-user-multi( + dropdown-toggle, + tooltip="#{translate('connected_users')}", + tooltip-placement="left" + ) + strong {{ onlineUsersArray.length }} + i.fa.fa-fw.fa-user + ul.dropdown-menu.pull-right + li.dropdown-header #{translate('connected_users')} + li(ng-repeat="user in onlineUsersArray") + a(href, ng-click="gotoUser(user)") + span.online-user( + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" + ) {{ user.name.slice(0,1) }} + | {{ user.name }} + a.btn.btn-full-height( href, diff --git a/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee b/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee index 785889ec66..f6633267c9 100644 --- a/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee +++ b/services/web/public/coffee/ide/online-users/OnlineUsersManager.coffee @@ -56,7 +56,11 @@ define [ user.doc = @ide.fileTreeManager.findEntityById(user.doc_id) if user.name?.trim().length == 0 - user.name = user.email + user.name = user.email.trim() + + user.initial = user.name?[0] + if !user.initial or user.initial == " " + user.initial = "?" @$scope.onlineUsersArray.push user diff --git a/services/web/public/stylesheets/app/editor/online-users.less b/services/web/public/stylesheets/app/editor/online-users.less index 17656e8e34..61640cc268 100644 --- a/services/web/public/stylesheets/app/editor/online-users.less +++ b/services/web/public/stylesheets/app/editor/online-users.less @@ -1,6 +1,8 @@ +@online-user-color: rgb(0, 170, 255); + .online-users { .online-user { - background-color: rgb(0, 170, 255); + background-color: @online-user-color; width: 24px; display: inline-block; height: 24px; @@ -11,4 +13,27 @@ border-radius: 3px; cursor: pointer; } + + .online-user-multi { + width: auto; + min-width: 24px; + padding-left: 8px; + padding-right: 5px; + } + + .dropdown-menu { + a { + // Override toolbar link styles + display: block; + padding: 4px 10px 5px; + margin: 1px 2px; + color: @text-color; + &:hover, &:active { + color: @text-color; + background-color: @gray-lightest; + text-shadow: none; + .box-shadow(none); + } + } + } } \ No newline at end of file