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) 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" 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/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index 652abbe64d..c992fec578 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" @@ -152,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/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/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 13cc60287f..d51a604637 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) -> @@ -19,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 @@ -33,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/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/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/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/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 9cf8525bac..4ac575af58 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 @@ -27,8 +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 - req.session.destroy() + 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/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/FileStore/FileStoreController.coffee b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee index 8182268d8e..c36ce22151 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreController.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreController.coffee @@ -1,21 +1,39 @@ 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) -> + 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 = 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? 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 + # 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 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/InactiveData/InactiveProjectController.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee new file mode 100644 index 0000000000..392e5e2f4e --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectController.coffee @@ -0,0 +1,25 @@ +InactiveProjectManager = require("./InactiveProjectManager") +logger = require("logger-sharelatex") + + +module.exports = + + deactivateOldProjects: (req, res)-> + logger.log "recived request to deactivate old projects" + numberOfProjectsToArchive = req.body.numberOfProjectsToArchive + ageOfProjects = req.body.ageOfProjects + InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)-> + if err? + res.sendStatus(500) + else + res.send(projectsDeactivated) + + + 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/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee new file mode 100644 index 0000000000..7885107c92 --- /dev/null +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -0,0 +1,56 @@ +async = require("async") +_ = require("underscore") +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, {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, active:project.active, "seeing if need to reactivate project" + + if project.active + 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 = 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(oldProjectDate) + .where("active").equals(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, (err)-> + if err? + logger.err err:err, "error deactivating projects" + callback err, projects + + + 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/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index 5c5903466e..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 @@ -23,22 +23,28 @@ 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")} 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.send 400 + return res.sendStatus 400 + delete req.session.resetToken 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 + res.send 404, {message: req.i18n.translate("password_reset_token_expired")} 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/Project/ProjectApiController.coffee b/services/web/app/coffee/Features/Project/ProjectApiController.coffee index b2214ccf8f..b16991ac62 100644 --- a/services/web/app/coffee/Features/Project/ProjectApiController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectApiController.coffee @@ -9,7 +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 - req.session.destroy() + 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..3c6264ecd6 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 = @@ -44,7 +46,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 +60,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 +101,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 +111,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") @@ -173,6 +175,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)-> @@ -181,11 +184,19 @@ 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() SubscriptionLocator.getUsersSubscription user_id, cb + activate: (cb)-> + InactiveProjectManager.reactivateProjectIfRequired 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" @@ -194,13 +205,16 @@ module.exports = ProjectController = 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.send 401 + return res.sendStatus 401 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/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/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/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) diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index ffabf60800..7738425c8e 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 = {active:false} + Project.update conditions, update, {}, (err)-> + if callback? + callback() + + markAsActive: (project_id, callback)-> + conditions = {_id:project_id} + update = {active:true} + Project.update conditions, update, {}, (err)-> + if callback? + callback() \ No newline at end of file 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/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/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/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) 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/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" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index a585a20e17..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.del '/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/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee index ef22e1764c..f256760808 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee @@ -17,11 +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 - req.session.destroy() + res.sendStatus 200 deleteUpdate: (req, res)-> @@ -32,11 +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 - req.session.destroy() + 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 @@ -49,8 +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) - req.session.destroy() + res.sendStatus(200) deleteProjectContents: (req, res, next = (error) ->) -> {project_id} = req.params @@ -59,8 +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) - req.session.destroy() + res.sendStatus(200) parseParams: parseParams = (req)-> path = req.params[0] diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index f964720115..f2d5bb94d1 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,33 @@ 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 = + + +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 : + group:group + 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)->)-> getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> @@ -27,9 +52,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 +89,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 +103,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 +113,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/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/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/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index a374939545..468832ca98 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -20,8 +20,8 @@ module.exports = user_id = req.session.user._id UserDeleter.deleteUser user_id, (err)-> if !err? - req.session.destroy() - res.send(200) + req.session?.destroy() + 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/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/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 4e67152b44..e0bb0724e1 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -39,38 +39,38 @@ for path in [ logger.log filePath:filePath, "file does not exist for fingerprints" -module.exports = (app)-> - app.use (req, res, next)-> +module.exports = (app, webRouter, apiRouter)-> + 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? @@ -94,16 +94,16 @@ module.exports = (app)-> return "" next() - app.use (req, res, next) -> - res.locals.csrfToken = req.session._csrf + webRouter.use (req, res, next) -> + 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)-> 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)-> 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 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/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 diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 2b2d80f0f2..8db396bca5 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") @@ -25,6 +33,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 @@ -34,56 +44,59 @@ else app = express() -csrf = express.csrf() -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') + +webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) +app.set 'views', __dirname + '/../../views' +app.set 'view engine', 'jade' +Modules.loadViewIncludes app -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) - app.use ReferalConnect.use - app.use express.methodOverride() - -expressLocals(app) - -app.configure 'production', -> - logger.info "Production Enviroment" - app.enable('view cache') +app.use bodyParser.urlencoded({ extended: true, limit: "2mb"}) +app.use bodyParser.json({limit: "2mb"}) +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 + saveUninitialized:false + secret:Settings.security.sessionSecret + proxy: Settings.behindProxy + cookie: + domain: Settings.cookieDomain + maxAge: Settings.cookieSessionLength + secure: Settings.secureCookie + store: sessionStore + key: Settings.cookieName +webRouter.use csrfProtection +webRouter.use translations.expressMiddlewear +webRouter.use translations.setLangBasedOnDomainMiddlewear + +# Measure expiry from last request, not last login +webRouter.use (req, res, next) -> + req.session.touch() + next() + +webRouter.use ReferalConnect.use +expressLocals(app, webRouter, apiRouter) + +if app.get('env') == 'production' + logger.info "Production Enviroment" + app.enable('view cache') + + + app.use (req, res, next)-> metrics.inc "http-request" crawlerLogger.log(req) @@ -96,12 +109,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 () -> @@ -112,7 +124,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/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 52901512c9..111ef40cfb 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} + active : { type: Boolean, default: true } owner_ref : {type:ObjectId, ref:'User'} collaberator_refs : [ type:ObjectId, ref:'User' ] readOnly_refs : [ type:ObjectId, ref:'User' ] @@ -26,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] @@ -42,6 +43,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 +56,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)-> diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index ae4121a456..22c1fbfe54 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -35,71 +35,72 @@ 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") module.exports = class Router - constructor: (app)-> + constructor: (webRouter, apiRouter)-> if !Settings.allowPublicAccess - app.all '*', AuthenticationController.requireGlobalLogin + webRouter.all '*', AuthenticationController.requireGlobalLogin - app.use(app.router) - 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) + + 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.del '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe - app.del '/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] @@ -107,78 +108,86 @@ module.exports = class Router req.params = params next() ), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi - app.del "/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.del '/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 + # 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 + apiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects + apiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject - 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') + 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 '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate - app.del '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate - app.ignoreCsrf('post', '/user/:user_id/update/*') - app.ignoreCsrf('delete', '/user/:user_id/update/*') + apiRouter.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument + apiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument + + apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate + apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate - app.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents - app.del '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents - app.ignoreCsrf('post', '/project/:project_id/contents/*') - app.ignoreCsrf('delete', '/project/:project_id/contents/*') + apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents + apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents - app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi + webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - app.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages - app.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage + webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages + webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage - app.get /learn(\/.*)?/, WikiController.getPage + 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 @@ -187,27 +196,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) + res.sendStatus(204) - app.get '*', ErrorController.notFound + webRouter.get '*', ErrorController.notFound 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/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/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/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade index 94183dc02d..70a09d9244 100644 --- a/services/web/app/views/project/list/side-bar.jade +++ b/services/web/app/views/project/list/side-bar.jade @@ -71,19 +71,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 diff --git a/services/web/package.json b/services/web/package.json index 28082dcd03..86cfd3f583 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -12,22 +12,30 @@ "dependencies": { "archiver": "0.9.0", "async": "0.6.2", + "base64-stream": "^0.1.2", + "basic-auth-connect": "^1.0.0", "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", - "fairy": "0.0.2", + "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", - "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.0.0", + "marked": "^0.3.3", + "method-override": "^2.3.3", + "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", - "mongoose": "3.8.28", + "mongoose": "4.1.0", + "multer": "^0.1.8", "node-uuid": "1.4.1", "nodemailer": "0.6.1", "optimist": "0.6.1", 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/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) -> 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() - diff --git a/services/web/public/img/about/geri.jpg b/services/web/public/img/about/geri.jpg new file mode 100644 index 0000000000..abf3af8e81 Binary files /dev/null and b/services/web/public/img/about/geri.jpg differ 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 0000000000..81f83d2d18 Binary files /dev/null and b/services/web/public/img/about/kiri_channon.jpg differ 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 0000000000..b353b2b6ad Binary files /dev/null and b/services/web/public/img/about/shane_kilkelly.jpg differ diff --git a/services/web/public/img/enago.png b/services/web/public/img/enago.png new file mode 100644 index 0000000000..879dd216c0 Binary files /dev/null and b/services/web/public/img/enago.png differ 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 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 diff --git a/services/web/public/stylesheets/app/templates.less b/services/web/public/stylesheets/app/templates.less index a08f5b8d3c..f7d490ff48 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; + .btn { + margin-left: 6px; + } } .searchResult { 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 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..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) @@ -321,7 +320,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 +329,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/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/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() 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/FileStore/FileStoreControllerTests.coffee b/services/web/test/UnitTests/coffee/FileStore/FileStoreControllerTests.coffee index fc665cee7a..fd83f73379 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", -> @@ -49,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 @@ -64,5 +65,66 @@ describe "FileStoreController", -> @res.setHeader.calledWith("Content-Disposition", "attachment; filename=#{@file.name}").should.equal true done() @controller.getFile @req, @res - + # Test behaviour around handling html files + ['.html', '.htm', '.xhtml'].forEach (extension) -> + describe "with a '#{extension}' file extension", -> + + beforeEach -> + @user_agent = 'A generic browser' + @file.name = "bad#{extension}" + @req.get = (key) => + if key == 'User-Agent' + @user_agent + + describe "from a non-ios 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 + + # None of these should trigger the iOS/html logic + ['x.html-is-rad', 'html.pdf', '.html-is-good-for-hidden-files', 'somefile'].forEach (filename) -> + describe "with filename as '#{filename}'", -> + + beforeEach -> + @user_agent = 'A generic browser' + @file.name = filename + @req.get = (key) => + if key == 'User-Agent' + @user_agent + + ['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach (browser) -> + describe "downloaded from #{browser}", -> + beforeEach -> + @user_agent = "Some #{browser} thing" + + 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 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..6befd7f70f --- /dev/null +++ b/services/web/test/UnitTests/coffee/InactiveData/InactiveProjectManagerTests.coffee @@ -0,0 +1,86 @@ +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() + archiveProject: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 = {active:false} + @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 active", (done)-> + @project.active = true + @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.archiveProject.callsArgWith(1) + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)=> + @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal true + done() + + it "should not call markAsInactive if there was a problem unarchiving", (done)-> + @DocstoreManager.archiveProject.callsArgWith(1, "errorrr") + @ProjectUpdateHandler.markAsInactive.callsArgWith(1) + + @InactiveProjectManager.deactivateProject @project_id, (err)=> + err.should.equal "errorrr" + @DocstoreManager.archiveProject.calledWith(@project_id).should.equal true + @ProjectUpdateHandler.markAsInactive.calledWith(@project_id).should.equal false + done() diff --git a/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee b/services/web/test/UnitTests/coffee/PasswordReset/PasswordResetControllerTests.coffee index a6590b31da..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 = {} @@ -51,7 +53,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 +80,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() @@ -86,9 +88,12 @@ 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.send = (code)=> + @res.sendStatus = (code)=> code.should.equal 200 @PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true done() @@ -104,7 +109,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,11 +118,51 @@ 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() @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) diff --git a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee index fbd50bb3ce..9476a80019 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectApiControllerTests.coffee @@ -36,14 +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 - - 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/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 77bfcebd35..d95dbb97b9 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 = + markAsOpened: 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" @@ -78,7 +84,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 +96,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 +108,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 +120,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 +132,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 +142,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 +150,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 +158,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 +250,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 +258,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 +266,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 @@ -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.markAsOpened.callsArgWith(1) + it "should render the project/editor page", (done)-> @res.render = (pageName, opts)=> @@ -317,7 +326,20 @@ 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 + + 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.markAsOpened.calledWith(@project_id).should.equal true + done() + @ProjectController.loadEditor @req, @res + 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', -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee index 3a275456e7..5922a027e3 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].active.should.equal false + 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].active.should.equal true + done() + + 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 e43737a478..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,10 +96,8 @@ describe 'TpdsController', -> .should.equal true it "should return a success", -> - @res.send.calledWith(200).should.equal true + @res.sendStatus.calledWith(200).should.equal true - it "should clear the session", -> - @req.session.destroy.called.should.equal true describe 'deleteProjectContents', -> beforeEach -> @@ -113,7 +111,7 @@ describe 'TpdsController', -> headers: "x-sl-update-source": @source = "github" @res = - send: sinon.stub() + sendStatus: sinon.stub() @TpdsController.deleteProjectContents @req, @res @@ -123,8 +121,5 @@ describe 'TpdsController', -> .should.equal true it "should return a success", -> - @res.send.calledWith(200).should.equal true - - it "should clear the session", -> - @req.session.destroy.called.should.equal true + @res.sendStatus.calledWith(200).should.equal true diff --git a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee index 2b7b691d8b..cd84080d17 100644 --- a/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee +++ b/services/web/test/UnitTests/coffee/ThirdPartyDataStore/TpdsUpdateSenderTests.coffee @@ -19,32 +19,55 @@ 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" + @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: - 'fairy':{connect:=>{queue:=>@requestQueuer}} - "settings-sharelatex": - siteUrl:siteUrl - httpAuthSiteUrl:httpAuthSiteUrl, - apis: - thirdPartyDataStore: {url: thirdPartyDataStoreApiUrl} - filestore: - url: filestoreUrl - docstore: - pubUrl: @docstoreUrl - redis:fairy:{} + "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', -> - 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 +82,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 +94,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 +106,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 +119,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 +130,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", 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", -> 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"