Merge branch 'master' into angular_1.3.15

This commit is contained in:
James Allen 2015-09-03 12:52:08 +01:00
commit aa06eb55b7
83 changed files with 1150 additions and 552 deletions

View file

@ -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=<regex> or --feature=<feature> for individual tests)', ['compile:server', 'compile:unit_tests', 'mochaTest:unit']
grunt.registerTask 'test:unit', 'Run the unit tests (use --grep=<regex> or --feature=<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)

View file

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

View file

@ -0,0 +1,5 @@
AnalyticsController = require('./AnalyticsController')
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.post '/event/:event', AnalyticsController.recordEvent

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')
apiRouter.post '/project/:Project_id/join', AuthenticationController.httpAuth, EditorHttpController.joinProject

View file

@ -1,6 +1,19 @@
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 =
@ -8,14 +21,19 @@ module.exports =
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

View file

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

View file

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

View file

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

View file

@ -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)->
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")}

View file

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

View file

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

View file

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

View file

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

View file

@ -54,7 +54,10 @@ module.exports =
findRootDoc : (opts, callback)->
getRootDoc = (project)=>
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

View file

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

View file

@ -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()

View file

@ -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 () ->

View file

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

View file

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

View file

@ -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
webRouter.get '/university', UniversityController.getIndexPage
webRouter.get '/university/*', UniversityController.getPage

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,24 +44,34 @@ else
app = express()
csrf = express.csrf()
ignoreCsrfRoutes = []
app.ignoreCsrf = (method, route) ->
ignoreCsrfRoutes.push new express.Route(method, route)
webRouter = express.Router()
apiRouter = express.Router()
app.configure () ->
if Settings.behindProxy
app.enable('trust proxy')
app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
app.set 'views', __dirname + '/../../views'
app.set 'view engine', 'jade'
Modules.loadViewIncludes app
app.use express.bodyParser(uploadDir: Settings.path.uploadFolder)
app.use translations.expressMiddlewear
app.use translations.setLangBasedOnDomainMiddlewear
app.use cookieParser
app.use express.session
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
@ -59,30 +79,23 @@ app.configure () ->
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
app.use (req, res, next) ->
webRouter.use (req, res, next) ->
req.session.touch()
next()
app.use (req, res, next) ->
for route in ignoreCsrfRoutes
if route.method == req.method?.toLowerCase() and route.match(req.path)
return next()
csrf(req, res, next)
webRouter.use ReferalConnect.use
expressLocals(app, webRouter, apiRouter)
app.use ReferalConnect.use
app.use express.methodOverride()
expressLocals(app)
app.configure 'production', ->
if app.get('env') == 'production'
logger.info "Production Enviroment"
app.enable('view cache')
app.use metrics.http.monitor(logger)
app.use RedirectManager
app.use OldAssetProxy
app.use (req, res, next)->
metrics.inc "http-request"
@ -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

View file

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

View file

@ -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)
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)
app.get '/blog', BlogController.getIndexPage
app.get '/blog/*', BlogController.getPage
if Settings.enableSubscriptions
app.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
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.get '/blog', BlogController.getIndexPage
webRouter.get '/blog/*', BlogController.getPage
app.del '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
app.del '/user', AuthenticationController.requireLogin(), UserController.deleteUser
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/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.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
app.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
app.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
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 '/Project/:Project_id', RateLimiterMiddlewear.rateLimit({
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
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
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 '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate
apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate
app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents
apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents
app.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages
app.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
app.get /learn(\/.*)?/, WikiController.getPage
webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages
webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
webRouter.get /learn(\/.*)?/, WikiController.getPage
#Admin Stuff
app.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
app.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser
app.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register
app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
app.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds
app.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser
app.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage
app.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages
webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
webRouter.get '/admin/register', SecurityManager.requestIsAdmin, AdminController.registerNewUser
webRouter.post '/admin/register', SecurityManager.requestIsAdmin, UserController.register
webRouter.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
webRouter.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
webRouter.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
webRouter.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds
webRouter.post '/admin/pollDropboxForUser', SecurityManager.requestIsAdmin, AdminController.pollDropboxForUser
webRouter.post '/admin/messages', SecurityManager.requestIsAdmin, AdminController.createMessage
webRouter.post '/admin/messages/clear', SecurityManager.requestIsAdmin, AdminController.clearMessages
app.get '/perfTest', (req,res)->
apiRouter.get '/perfTest', (req,res)->
res.send("hello")
req.session.destroy()
app.get '/status', (req,res)->
apiRouter.get '/status', (req,res)->
res.send("websharelatex is up")
req.session.destroy()
app.get '/health_check', HealthCheckController.check
app.get '/health_check/redis', HealthCheckController.checkRedis
app.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) ->
webRouter.get '/health_check', HealthCheckController.check
webRouter.get '/health_check/redis', HealthCheckController.checkRedis
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

View file

@ -57,6 +57,7 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
ng-show="onlineUsersArray.length > 0"
ng-controller="OnlineUsersController"
)
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%)' }",
@ -67,6 +68,24 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
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,
ng-if="permissions.admin",

View file

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

View file

@ -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')
| &nbsp;&nbsp;
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")}.

View file

@ -71,6 +71,20 @@
strong #{translate("create_your_first_project")}
- if (showUserDetailsArea)
- if (Math.random() < 0.5)
.row-spaced
hr
.card.card-thin
p.text-center.small
| <strong>Python</strong> or <strong>R</strong> user?
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")

View file

@ -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",

View file

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

View file

@ -22,16 +22,31 @@ 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
$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 :("

View file

@ -3,7 +3,6 @@ define [
"main/user-details"
"main/account-settings"
"main/account-upgrade"
"main/templates"
"main/plans"
"main/group-members"
"main/scribtex-popup"

View file

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -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";

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}

View file

@ -49,6 +49,9 @@
.template-details-section {
padding-bottom: 20px;
.btn {
margin-left: 6px;
}
}
.searchResult {

View file

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

View file

@ -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", ->

View file

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

View file

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

View file

@ -12,6 +12,9 @@ Errors = require "../../../../app/js/errors"
describe "DocumentController", ->
beforeEach ->
@DocumentController = SandboxedModule.require modulePath, requires:
"logger-sharelatex":
log:->
err:->
"../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
@res = new MockResponse()
@req = new MockRequest()

View file

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

View file

@ -26,6 +26,7 @@ describe "FileStoreController", ->
Project_id: @project_id
File_id: @file_id
query: "query string here"
get: (key) -> undefined
@res =
setHeader: sinon.stub()
@file =
@ -65,4 +66,65 @@ describe "FileStoreController", ->
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

View file

@ -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()

View file

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

View file

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

View file

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

View file

@ -160,6 +160,13 @@ describe 'project model', ->
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', ->
it 'should take a doc path and return the element for a root level document', (done)->

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

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

View file

@ -19,13 +19,12 @@ filestoreUrl = "filestore.sharelatex.com"
describe 'TpdsUpdateSender', ->
beforeEach ->
@requestQueuer = regist:(queue, meth, opts, callback)->
@requestQueuer = (queue, meth, opts, callback)->
project = {owner_ref:user_id,readOnly_refs:[read_only_ref_1], collaberator_refs:[collaberator_ref_1]}
@Project = findById:sinon.stub().callsArgWith(2, null, project)
@docstoreUrl = "docstore.sharelatex.env"
@updateSender = SandboxedModule.require modulePath, requires:
'fairy':{connect:=>{queue:=>@requestQueuer}}
"settings-sharelatex":
@request = sinon.stub().returns(pipe:->)
@settings =
siteUrl:siteUrl
httpAuthSiteUrl:httpAuthSiteUrl,
apis:
@ -34,17 +33,41 @@ describe 'TpdsUpdateSender', ->
url: filestoreUrl
docstore:
pubUrl: @docstoreUrl
redis:fairy:{}
@updateSender = SandboxedModule.require modulePath, requires:
"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",

View file

@ -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", ->

View file

@ -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()

View file

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