Intial open source comment

This commit is contained in:
James Allen 2014-02-12 10:23:40 +00:00
commit 8715690ce9
1694 changed files with 426896 additions and 0 deletions

69
services/web/.gitignore vendored Normal file
View file

@ -0,0 +1,69 @@
Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# Logs and databases #
######################
*.log
*.sql
*.sqlite
# OS generated files #
######################
.DS_Store?
ehthumbs.db
Icon?
Thumbs.db
node_modules/*
data/*
app.js
app/js/*
test/UnitTests/js/*
test/smoke/js/*
cookies.txt
requestQueueWorker.js
TpdsWorker.js
BackgroundJobsWorker.js
public/js/history/versiondetail.js
!public/js/libs/*
public/js/*
!public/js/ace/*
!public/js/libs/*
public/js/editor.js
public/js/home.js
public/js/forms.js
public/js/gui.js
public/js/admin.js
public/js/history/*
public/stylesheets/mainStyle.css
public/stylesheets/plans.css
public/minjs/
public/js/main.js
Gemfile.lock
*.swp
.DS_Store

4
services/web/.npmignore Normal file
View file

@ -0,0 +1,4 @@
node_modules
data
log
public/minjs

View file

@ -0,0 +1,27 @@
settings = require('settings-sharelatex')
SubscriptionBackgroundTasks = require("./app/js/Features/Subscription/SubscriptionBackgroundTasks")
TpdsPollingBackgroundTasks = require("./app/js/Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks")
AutomaticSnapshotManager = require("./app/js/Features/Versioning/AutomaticSnapshotManager")
time =
oneHour : 60 * 60 * 1000
fifteenMinutes : 15 * 60 * 1000
thirtySeconds : 30 * 1000
betweenThirtyAndFiveHundredSeconds: =>
random = Math.floor(Math.random() * 500) * 1000
if random < time.thirtySeconds
return time.betweenThirtyAndFiveHundredSeconds()
else
return random
runPeriodically = (funcToRun, periodLength)->
recursiveReference = ->
funcToRun ->
setTimeout recursiveReference, periodLength
setTimeout recursiveReference, 0
# TODO: Remove this one month after the ability to start free trials was removed
runPeriodically ((cb) -> SubscriptionBackgroundTasks.downgradeExpiredFreeTrials(cb)), time.oneHour
runPeriodically ((cb) -> TpdsPollingBackgroundTasks.pollUsersWithDropbox(cb)), time.fifteenMinutes
runPeriodically ((cb) -> AutomaticSnapshotManager.takeAutomaticSnapshots(cb)), time.thirtySeconds

View file

@ -0,0 +1,197 @@
fs = require "fs"
module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-coffee'
grunt.loadNpmTasks 'grunt-contrib-less'
grunt.loadNpmTasks 'grunt-contrib-clean'
grunt.loadNpmTasks 'grunt-mocha-test'
grunt.loadNpmTasks 'grunt-available-tasks'
grunt.loadNpmTasks 'grunt-contrib-requirejs'
grunt.loadNpmTasks 'grunt-execute'
grunt.loadNpmTasks 'grunt-bunyan'
grunt.initConfig
execute:
app:
src: "app.js"
coffee:
app_dir:
expand: true,
flatten: false,
cwd: 'app/coffee',
src: ['**/*.coffee'],
dest: 'app/js/',
ext: '.js'
app:
src: 'app.coffee'
dest: 'app.js'
sharejs:
options:
join: true
files:
"public/js/libs/sharejs.js": [
"public/coffee/editor/ShareJSHeader.coffee"
"public/coffee/editor/sharejs/types/helpers.coffee"
"public/coffee/editor/sharejs/types/text.coffee"
"public/coffee/editor/sharejs/types/text-api.coffee"
"public/coffee/editor/sharejs/types/json.coffee"
"public/coffee/editor/sharejs/types/json-api.coffee"
"public/coffee/editor/sharejs/client/microevent.coffee"
"public/coffee/editor/sharejs/client/doc.coffee"
"public/coffee/editor/sharejs/client/ace.coffee"
]
client:
expand: true,
flatten: false,
cwd: 'public/coffee',
src: ['**/*.coffee'],
dest: 'public/js/',
ext: '.js'
smoke_tests:
expand: true,
flatten: false,
cwd: 'test/smoke/coffee',
src: ['**/*.coffee'],
dest: 'test/smoke/js/',
ext: '.js'
unit_tests:
expand: true,
flatten: false,
cwd: 'test/UnitTests/coffee',
src: ['**/*.coffee'],
dest: 'test/UnitTests/js/',
ext: '.js'
less:
app:
files:
"public/stylesheets/mainStyle.css": "public/stylesheets/mainStyle.less"
plans:
files:
"public/stylesheets/plans.css": "public/stylesheets/less/plans.less"
requirejs:
compile:
options:
appDir: "public/js"
baseUrl: "./"
dir: "public/minjs"
inlineText: false
preserveLicenseComments: false
paths:
"underscore": "libs/underscore"
"jquery": "libs/jquery"
shim:
"libs/backbone":
deps: ["libs/underscore"]
"libs/pdfListView/PdfListView":
deps: ["libs/pdf"]
"libs/pdf":
deps: ["libs/compatibility"]
skipDirOptimize: true
modules: [
{
name: "main",
exclude: ["jquery"]
}, {
name: "ide",
exclude: ["jquery"]
}, {
name: "home",
exclude: ["jquery"]
}, {
name: "list",
exclude: ["jquery"]
}
]
clean:
app: ["app/js"]
unit_tests: ["test/UnitTests/js"]
mochaTest:
unit:
src: ["test/UnitTests/js/#{grunt.option('feature') or '**'}/*.js"]
options:
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
smoke:
src: ['test/smoke/js/**/*.js']
options:
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
availabletasks:
tasks:
options:
filter: 'exclude',
tasks: [
'coffee'
'less'
'clean'
'mochaTest'
'availabletasks'
'wrap_sharejs'
'requirejs'
'execute'
'bunyan'
]
groups:
"Compile tasks": [
"compile:server"
"compile:client"
"compile:tests"
"compile"
"compile:unit_tests"
"compile:smoke_tests"
"compile:css"
"compile:minify"
"install"
]
"Test tasks": [
"test:unit"
]
"Run tasks": [
"run"
"default"
]
"Misc": [
"help"
]
grunt.registerTask 'wrap_sharejs', 'Wrap the compiled ShareJS code for AMD module loading', () ->
content = fs.readFileSync "public/js/libs/sharejs.js"
fs.writeFileSync "public/js/libs/sharejs.js", """
define(["ace/range"], function() {
#{content}
return window.sharejs;
});
"""
grunt.registerTask 'help', 'Display this help list', 'availabletasks'
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir']
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs']
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs']
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests']
grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests']
grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css']
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:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke']
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'bunyan', 'execute']
grunt.registerTask 'default', 'run'

View file

@ -0,0 +1,98 @@
async = require('async')
request = require('request')
keys = require('./app/js/infrastructure/Keys')
settings = require('settings-sharelatex')
logger = require('logger-sharelatex')
_ = require('underscore')
childProcess = require("child_process")
metrics = require("./app/js/infrastructure/Metrics")
fiveMinutes = 5 * 60 * 1000
processingFuncs =
sendDoc : (options, callback)->
if !options.docLines? || options.docLines.length == 0
logger.err options:options, "doc lines not added to options for processing"
return callback()
docLines = options.docLines.reduce (singleLine, line)-> "#{singleLine}\n#{line}"
post = request(options)
post.on 'error', (err)->
if err?
callback(err)
else
callback()
post.on 'end', callback
post.write(docLines, 'utf-8')
standardHttpRequest: (options, callback)->
request options, (err, reponse, body)->
if err?
callback(err)
else
callback()
pipeStreamFrom: (options, callback)->
if options.filePath == "/droppy/main.tex"
request options.streamOrigin, (err,res, body)->
logger.log options:options, body:body
origin = request(options.streamOrigin)
origin.on 'error', (err)->
logger.error err:err, options:options, "something went wrong in pipeStreamFrom origin"
if err?
callback(err)
else
callback()
dest = request(options)
origin.pipe(dest)
dest.on "error", (err)->
logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest"
if err?
callback(err)
else
callback()
dest.on 'end', callback
workerRegistration = (groupKey, method, options, callback)->
callback = _.once callback
setTimeout callback, fiveMinutes
metrics.inc "tpds-worker-processing"
logger.log groupKey:groupKey, method:method, options:options, "processing http request from queue"
processingFuncs[method] options, (err)->
if err?
logger.err err:err, user_id:groupKey, method:method, options:options, "something went wrong processing tpdsUpdateSender update"
return callback("skip-after-retry")
callback()
setupWebToTpdsWorkers = (queueName)->
logger.log worker_count:worker_count, queueName:queueName, "fairy workers"
worker_count = 4
while worker_count-- > 0
workerQueueRef = require('fairy').connect(settings.redis.fairy).queue(queueName)
workerQueueRef.polling_interval = 100
workerQueueRef.regist workerRegistration
cleanupPreviousQueues = (queueName, callback)->
#cleanup queues then setup workers
fairy = require('fairy').connect(settings.redis.fairy)
queuePrefix = "FAIRY:QUEUED:#{queueName}:"
fairy.redis.keys "#{queuePrefix}*", (err, keys)->
logger.log "#{keys.length} fairy queues need cleanup"
queueNames = keys.map (key)->
key.replace queuePrefix, ""
cleanupJobs = queueNames.map (projectQueueName)->
return (cb)->
cleanup = childProcess.fork(__dirname + '/cleanup.js', [queueName, projectQueueName])
cleanup.on 'exit', cb
async.series cleanupJobs, callback
cleanupPreviousQueues keys.queue.web_to_tpds_http_requests, ->
setupWebToTpdsWorkers keys.queue.web_to_tpds_http_requests
cleanupPreviousQueues keys.queue.tpds_to_web_http_requests, ->
setupWebToTpdsWorkers keys.queue.tpds_to_web_http_requests

43
services/web/app.coffee Normal file
View file

@ -0,0 +1,43 @@
Settings = require('settings-sharelatex')
logger = require 'logger-sharelatex'
logger.initialize("web-sharelatex")
logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user
logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project
Server = require("./app/js/infrastructure/Server")
BackgroundTasks = require("./app/js/infrastructure/BackgroundTasks")
Errors = require "./app/js/errors"
argv = require("optimist")
.options("user", {alias : "u", description : "Run the server with permissions of the specified user"})
.options("group", {alias : "g", description : "Run the server with permissions of the specified group"})
.usage("Usage: $0")
.argv
Server.app.use (error, req, res, next) ->
logger.error err: error
res.statusCode = error.status or 500
if res.statusCode == 500
res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at team@sharelatex.com")
else
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"
BackgroundTasks.run()
port = Settings.port or Settings.internal?.web?.port or 3000
Server.server.listen port, ->
logger.info("web-sharelatex listening on port #{port}")
logger.info("#{require('http').globalAgent.maxSockets} sockets enabled")
if argv.user
process.setuid argv.user
logger.info "Running as user: #{argv.user}"
if argv.group
process.setgid argv.group
logger.info "Running as group: #{argv.group}"

View file

@ -0,0 +1,69 @@
Settings = require 'settings-sharelatex'
if Settings.analytics?.mixpanel?
Mixpanel = require("mixpanel").init(Settings.analytics.mixpanel.token)
else
Mixpanel = null
logger = require "logger-sharelatex"
async = require 'async'
module.exports = AnalyticsManager =
track: (user, event, properties, callback = (error)->) ->
properties.distinct_id = @getDistinctId user
properties.mp_name_tag = user.email if user.email?
logger.log user_id: properties.distinct_id, event: event, properties: properties, "tracking event"
Mixpanel?.track event, properties
callback()
set: (user, properties, callback = (error)->) ->
properties["$first_name"] = user.first_name if user.first_name?
properties["$last_name"] = user.last_name if user.last_name?
properties["$email"] = user.email if user.email?
Mixpanel?.people.set @getDistinctId(user), properties
callback()
increment: (user, property, amount, callback = (error)->) ->
Mixpanel?.people.increment @getDistinctId(user), property, amount
callback()
# TODO: Remove this one month after the ability to start free trials was removed
trackFreeTrialExpired: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "free trial expired", {}, callback
(callback) => @set user, { free_trial_expired_at: new Date() }, callback
], callback
trackSubscriptionStarted: (user, plan_code, callback = (error)->) ->
async.series [
(callback) => @track user, "subscribed", plan_code: plan_code, callback
(callback) => @set user, { plan_code: plan_code, subscribed_at: new Date() }, callback
], callback
trackSubscriptionCancelled: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "cancelled", callback
(callback) => @set user, { cancelled_at: new Date() }, callback
], callback
trackLogIn: (user, callback = (error)->) ->
async.series [
(callback) => @track user, "logged in", {}, callback
(callback) => @set user, { last_logged_id: new Date() }, callback
], callback
trackOpenEditor: (user, project, callback = (error)->) ->
async.series [
(callback) => @set user, { last_opened_editor: new Date() }, callback
(callback) => @increment user, "editor_opens", 1, callback
], callback
trackReferral: (user, referal_source, referal_medium, callback = (error) ->) ->
async.series [
(callback) =>
@track user, "Referred another user", { source: referal_source, medium: referal_medium }, callback
(callback) =>
@track user, "Referred another user via #{referal_source}", { medium: referal_medium }, callback
], callback
getDistinctId: (user) -> user.id || user._id || user

View file

@ -0,0 +1,113 @@
AuthenticationManager = require ("./AuthenticationManager")
LoginRateLimiter = require("../Security/LoginRateLimiter")
UserGetter = require "../User/UserGetter"
UserUpdater = require "../User/UserUpdater"
Metrics = require('../../infrastructure/Metrics')
logger = require("logger-sharelatex")
querystring = require('querystring')
Url = require("url")
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
email = req.body?.email?.toLowerCase()
password = req.body?.password
redir = Url.parse(req.body?.redir or "/project").path
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
if !isAllowed
logger.log email:email, "too many login requests"
res.statusCode = 429
return res.send
message:
text: 'This account has had too many login requests, <br> please wait 2 minutes before trying to log in again',
type: 'error'
AuthenticationManager.authenticate email: email, password, (error, user) ->
return next(error) if error?
if user?
LoginRateLimiter.recordSuccessfulLogin email
AuthenticationController._recordSuccessfulLogin user._id
AuthenticationController._establishUserSession req, user, (error) ->
return next(error) if error?
logger.log email: email, user_id: user._id.toString(), "successful log in"
res.send redir: redir
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
res.send message:
text: 'Your email or password were incorrect. Please try again',
type: 'error'
getAuthToken: (req, res, next = (error) ->) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
return next(error) if error?
AuthenticationManager.getAuthToken user_id, (error, auth_token) ->
return next(error) if error?
res.send(auth_token)
getLoggedInUserId: (req, callback = (error, user_id) ->) ->
callback null, req.session.user._id.toString()
getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) ->
if req.session?.user?._id?
query = req.session.user._id
else if req.query?.auth_token? and options.allow_auth_token
query = { auth_token: req.query.auth_token }
else
return callback null, null
UserGetter.getUser query, callback
requireLogin: (options = {allow_auth_token: false, load_from_db: false}) ->
doRequest = (req, res, next = (error) ->) ->
load_from_db = options.load_from_db
if req.query?.auth_token? and options.allow_auth_token
load_from_db = true
if load_from_db
AuthenticationController.getLoggedInUser req, { allow_auth_token: options.allow_auth_token }, (error, user) ->
return next(error) if error?
return AuthenticationController._redirectToRegisterPage(req, res) if !user?
req.user = user
return next()
else
if !req.session.user?
return AuthenticationController._redirectToRegisterPage(req, res)
else
req.user = req.session.user
return next()
return doRequest
_redirectToRegisterPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to register page"
req.query.redir = req.path
url = "/register?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
_recordSuccessfulLogin: (user_id, callback = (error) ->) ->
UserUpdater.updateUser user_id.toString(), {
$set: { "lastLoggedIn": new Date() },
$inc: { "loginCount": 1 }
}, (error) ->
callback(error) if error?
Metrics.inc "user.login.success"
callback()
_recordFailedLogin: (callback = (error) ->) ->
Metrics.inc "user.login.failed"
callback()
_establishUserSession: (req, user, callback = (error) ->) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
email: user.email
referal_id: user.referal_id
req.session.user = lightUser
req.session.justLoggedIn = true
req.session.save callback

View file

@ -0,0 +1,54 @@
Settings = require 'settings-sharelatex'
User = require("../../models/User").User
{db, ObjectId} = require("../../infrastructure/mongojs")
crypto = require 'crypto'
bcrypt = require 'bcrypt'
module.exports = AuthenticationManager =
authenticate: (query, password, callback = (error, user) ->) ->
# Using Mongoose for legacy reasons here. The returned User instance
# gets serialized into the session and there may be subtle differences
# between the user returned by Mongoose vs mongojs (such as default values)
User.findOne query, (error, user) =>
return callback(error) if error?
if user?
if user.hashedPassword?
bcrypt.compare password, user.hashedPassword, (error, match) ->
return callback(error) if error?
if match
callback null, user
else
callback null, null
else
callback null, null
else
callback null, null
setUserPassword: (user_id, password, callback = (error) ->) ->
bcrypt.genSalt 7, (error, salt) ->
return callback(error) if error?
bcrypt.hash password, salt, (error, hash) ->
return callback(error) if error?
db.users.update({
_id: ObjectId(user_id.toString())
}, {
$set: hashedPassword: hash
$unset: password: true
}, callback)
getAuthToken: (user_id, callback = (error, auth_token) ->) ->
db.users.findOne { _id: ObjectId(user_id.toString()) }, { auth_token : true }, (error, user) =>
return callback(error) if error?
return callback(new Error("user could not be found: #{user_id}")) if !user?
if user.auth_token?
callback null, user.auth_token
else
@_createSecureToken (error, auth_token) ->
db.users.update { _id: ObjectId(user_id.toString()) }, { $set : auth_token: auth_token }, (error) ->
return callback(error) if error?
callback null, auth_token
_createSecureToken: (callback = (error, token) ->) ->
crypto.randomBytes 48, (error, buffer) ->
return callback(error) if error?
callback null, buffer.toString("hex")

View file

@ -0,0 +1,49 @@
ProjectGetter = require "../Project/ProjectGetter"
ProjectHandler = require "../../handlers/ProjectHandler"
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) ->
return next(error) if error?
CollaboratorsController._formatCollaborators project, (error, collaborators) ->
return next(error) if error?
res.send(JSON.stringify(collaborators))
removeSelfFromProject: (req, res, next = (error) ->) ->
user_id = req.session?.user?._id
if !user_id?
return next(new Error("User should be logged in"))
ProjectHandler::removeUserFromProject req.params.project_id, user_id, (error) ->
return next(error) if error?
res.redirect "/project"
_formatCollaborators: (project, callback = (error, collaborators) ->) ->
collaborators = []
pushCollaborator = (user, permissions, owner) ->
collaborators.push {
id: user._id.toString()
first_name: user.first_name
last_name: user.last_name
email: user.email
permissions: permissions
owner: owner
}
if project.owner_ref?
pushCollaborator(project.owner_ref, ["read", "write", "admin"], true)
if project.collaberator_refs? and project.collaberator_refs.length > 0
for user in project.collaberator_refs
pushCollaborator(user, ["read", "write"], false)
if project.readOnly_refs? and project.readOnly_refs.length > 0
for user in project.readOnly_refs
pushCollaborator(user, ["read"], false)
callback null, collaborators

View file

@ -0,0 +1,97 @@
Path = require "path"
async = require "async"
Settings = require "settings-sharelatex"
request = require('request')
Project = require("../../models/Project").Project
logger = require "logger-sharelatex"
url = require("url")
module.exports = ClsiManager =
sendRequest: (project_id, callback = (error, success) ->) ->
Project.findById project_id, (error, project) ->
return callback(error) if error?
ClsiManager._buildRequest project, (error, req) ->
return callback(error) if error?
logger.log project_id: project_id, "sending compile to CLSI"
ClsiManager._postToClsi project_id, req, (error, response) ->
return callback(error) if error?
logger.log project_id: project_id, response: response, "received compile response from CLSI"
callback(
null
(response?.compile?.status == "success")
ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
)
getLogLines: (project_id, callback = (error, lines) ->) ->
request "#{Settings.apis.clsi.url}/project/#{project_id}/output/output.log", (error, response, body) ->
return callback(error) if error?
callback null, body?.split("\n") or []
_postToClsi: (project_id, req, callback = (error, response) ->) ->
request.post {
url: "#{Settings.apis.clsi.url}/project/#{project_id}/compile"
json: req
jar: false
}, (error, response, body) ->
callback error, body
_parseOutputFiles: (project_id, rawOutputFiles = []) ->
outputFiles = []
for file in rawOutputFiles
outputFiles.push
path: url.parse(file.url).path.replace("/project/#{project_id}/output/", "")
type: file.type
return outputFiles
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
_buildRequest: (project, callback = (error, request) ->) ->
if project.compiler not in @VALID_COMPILERS
project.compiler = "pdflatex"
resources = []
rootResourcePath = null
addDoc = (basePath, doc, callback = (error) ->) ->
path = Path.join(basePath, doc.name)
resources.push
path: path
content: doc.lines.join("\n")
if doc._id.toString() == project.rootDoc_id.toString()
rootResourcePath = path
callback()
addFile = (basePath, file, callback = (error) ->) ->
resources.push
path: Path.join(basePath, file.name)
url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}"
modified: file.created?.getTime()
callback()
addFolder = (basePath, folder, callback = (error) ->) ->
jobs = []
for doc in folder.docs
do (doc) ->
jobs.push (callback) -> addDoc(basePath, doc, callback)
for file in folder.fileRefs
do (file) ->
jobs.push (callback) -> addFile(basePath, file, callback)
for childFolder in folder.folders
do (childFolder) ->
jobs.push (callback) -> addFolder(Path.join(basePath, childFolder.name), childFolder, callback)
async.series jobs, callback
addFolder "", project.rootFolder[0], (error) ->
if !rootResourcePath?
callback new Error("no root document exists")
else
callback null, {
compile:
options:
compiler: project.compiler
rootResourcePath: rootResourcePath
resources: resources
}

View file

@ -0,0 +1,45 @@
Metrics = require "../../infrastructure/Metrics"
Project = require("../../models/Project").Project
CompileManager = require("./CompileManager")
logger = require "logger-sharelatex"
request = require "request"
Settings = require "settings-sharelatex"
module.exports = CompileController =
downloadPdf: (req, res, next = (error) ->)->
Metrics.inc "pdf-downloads"
project_id = req.params.Project_id
Project.findById project_id, {name: 1}, (err, project)->
res.contentType("application/pdf")
if !!req.query.popupDownload
logger.log project_id: project_id, "download pdf as popup download"
res.header('Content-Disposition', "attachment; filename=#{project.getSafeProjectName()}.pdf")
else
logger.log project_id: project_id, "download pdf to embed in browser"
res.header('Content-Disposition', "filename=#{project.getSafeProjectName()}.pdf")
CompileController.proxyToClsi("/project/#{project_id}/output/output.pdf", req, res, next)
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
url = "/project/#{project_id}/output/output.pdf"
CompileController.proxyToClsi url, req, res, next
getFileFromClsi: (req, res, next = (error) ->) ->
CompileController.proxyToClsi("/project/#{req.params.Project_id}/output/#{req.params.file}", req, res, next)
proxyToClsi: (url, req, res, next = (error) ->) ->
logger.log url: url, "proxying to CLSI"
url = "#{Settings.apis.clsi.url}#{url}"
oneMinute = 60 * 1000
proxy = request.get(url: url, timeout: oneMinute)
proxy.pipe(res)
proxy.on "error", (error) ->
logger.error err: error, url: url, "CLSI proxy error"

View file

@ -0,0 +1,82 @@
Settings = require('settings-sharelatex')
redis = require('redis')
rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host)
rclient.auth(Settings.redis.web.password)
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
Project = require("../../models/Project").Project
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
ClsiManager = require "./ClsiManager"
Metrics = require('../../infrastructure/Metrics')
logger = require("logger-sharelatex")
RateLimiter = require("ratelimiter")
module.exports = CompileManager =
compile: (project_id, user_id, opt = {}, _callback = (error) ->) ->
timer = new Metrics.Timer("editor.compile")
callback = (args...) ->
timer.done()
_callback(args...)
@_checkIfAutoCompileLimitHasBeenHit opt.isAutoCompile, (err, canCompile)->
if !canCompile
err = {rateLimitHit:true}
return callback(err)
logger.log project_id: project_id, user_id: user_id, "compiling project"
CompileManager._checkIfRecentlyCompiled project_id, user_id, (error, recentlyCompiled) ->
return callback(error) if error?
if recentlyCompiled
return callback new Error("project was recently compiled so not continuing")
CompileManager._ensureRootDocumentIsSet project_id, (error) ->
return callback(error) if error?
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
ClsiManager.sendRequest project_id, (error, success, outputFiles) ->
return callback(error) if error?
logger.log files: outputFiles, "output files"
callback(null, success, outputFiles)
getLogLines: (project_id, callback)->
Metrics.inc "editor.raw-logs"
ClsiManager.getLogLines project_id, (error, logLines)->
return callback(error) if error?
callback null, logLines
COMPILE_DELAY: 1 # seconds
_checkIfRecentlyCompiled: (project_id, user_id, callback = (error, recentlyCompiled) ->) ->
key = "compile:#{project_id}:#{user_id}"
rclient.set key, true, "EX", @COMPILE_DELAY, "NX", (error, ok) ->
return callback(error) if error?
if ok == "OK"
return callback null, false
else
return callback null, true
_checkIfAutoCompileLimitHasBeenHit: (isAutoCompile, callback = (err, canCompile)->)->
if !isAutoCompile
return callback(null, true)
key = "auto_compile_rate_limit"
ten_seconds = (10 * 1000)
limit = new RateLimiter(db:rclient, id:key, max:7, duration:ten_seconds)
limit.get (err, limit)->
Metrics.inc("compile.autocompile.rateLimitCheck")
if limit.remaining > 0 and !err?
canCompile = true
else
canCompile = false
Metrics.inc("compile.autocompile.rateLimitHit")
logger.log canCompile:canCompile, limit:limit, "checking if auto compile limit has been hit"
callback err, canCompile
_ensureRootDocumentIsSet: (project_id, callback = (error) ->) ->
Project.findById project_id, 'rootDoc_id', (error, project)=>
return callback(error) if error?
if !project?
return callback new Error("project not found")
if project.rootDoc_id?
callback()
else
ProjectRootDocManager.setRootDocAutomatically project_id, callback

View file

@ -0,0 +1,141 @@
request = require 'request'
request = request.defaults()
async = require 'async'
settings = require 'settings-sharelatex'
_ = require 'underscore'
async = require 'async'
logger = require('logger-sharelatex')
metrics = require('../../infrastructure/Metrics')
slReqIdHelper = require('soa-req-id')
redis = require('redis')
rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host)
rclient.auth(settings.redis.web.password)
Project = require("../../models/Project").Project
ProjectLocator = require('../../Features/Project/ProjectLocator')
module.exports =
queueChange : (project_id, doc_id, change, sl_req_id, callback = ()->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
jsonChange = JSON.stringify change
rclient.rpush keys.pendingUpdates(doc_id:doc_id), jsonChange, (error)->
return callback(error) if error?
doc_key = keys.combineProjectIdAndDocId(project_id, doc_id)
rclient.sadd keys.docsWithPendingUpdates, doc_key, (error) ->
return callback(error) if error?
rclient.publish "pending-updates", doc_key, callback
flushProjectToMongo: (project_id, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, sl_req_id:sl_req_id, "flushing project from document updater"
timer = new metrics.Timer("flushing.mongo.project")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/flush"
request.post url, (error, res, body)->
if error?
logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "error flushing project from document updater"
return callback(error)
else if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id: project_id, sl_req_id: sl_req_id, "flushed project from document updater"
return callback(null)
else
error = new Error("document updater returned a failure status code: #{res.statusCode}")
logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error)
flushProjectToMongoAndDelete: (project_id, sl_req_id, callback = ()->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, sl_req_id:sl_req_id, "deleting project from document updater"
timer = new metrics.Timer("delete.mongo.project")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}"
request.del url, (error, res, body)->
if error?
logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "error deleting project from document updater"
return callback(error)
else if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id: project_id, sl_req_id: sl_req_id, "deleted project from document updater"
return callback(null)
else
error = new Error("document updater returned a failure status code: #{res.statusCode}")
logger.error err: error, project_id: project_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error)
deleteDoc : (project_id, doc_id, sl_req_id, callback = ()->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "deleting doc from document updater"
timer = new metrics.Timer("delete.mongo.doc")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
request.del url, (error, res, body)->
if error?
logger.error err: error, project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "error deleting doc from document updater"
return callback(error)
else if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "deleted doc from document updater"
return callback(null)
else
error = new Error("document updater returned a failure status code: #{res.statusCode}")
logger.error err: error, project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error)
getDocument: (project_id, doc_id, fromVersion, sl_req_id, callback = (error, exists, doclines, version) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
timer = new metrics.Timer("get-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "getting doc from document updater"
request.get url, (error, res, body)->
timer.done()
if error?
logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error getting doc from doc updater"
return callback(error)
if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id:project_id, doc_id:doc_id, "got doc from document document updater"
try
body = JSON.parse(body)
catch error
return callback(error)
callback null, body.lines, body.version, body.ops
else
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
setDocument : (project_id, doc_id, docLines, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
timer = new metrics.Timer("set-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
body =
url: url
json:
lines: docLines
logger.log project_id:project_id, doc_id: doc_id, sl_req_id:sl_req_id, "setting doc in document updater"
request.post body, (error, res, body)->
timer.done()
if error?
logger.error err:error, url:url, project_id:project_id, doc_id:doc_id, "error setting doc in doc updater"
return callback(error)
if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id: project_id, doc_id: doc_id, sl_req_id: sl_req_id, "set doc in document updater"
return callback(null)
else
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
getNumberOfDocsInMemory : (callback)->
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
try
body = JSON.parse body
catch err
logger.err err:err, "error parsing response from doc updater about the total number of docs"
callback(err, body?.total)
PENDINGUPDATESKEY = "PendingUpdates"
DOCLINESKEY = "doclines"
DOCIDSWITHPENDINGUPDATES = "DocsWithPendingUpdates"
keys =
pendingUpdates : (op) -> "#{PENDINGUPDATESKEY}:#{op.doc_id}"
docsWithPendingUpdates: DOCIDSWITHPENDINGUPDATES
docLines : (op) -> "#{DOCLINESKEY}:#{op.doc_id}"
combineProjectIdAndDocId: (project_id, doc_id) -> "#{project_id}:#{doc_id}"

View file

@ -0,0 +1,36 @@
ProjectLocator = require "../Project/ProjectLocator"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
Errors = require "../../errors"
logger = require("logger-sharelatex")
module.exports =
getDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
ProjectLocator.findElement project_id: project_id, element_id: doc_id, type: "doc", (error, doc) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
res.type "json"
res.send JSON.stringify {
lines: doc.lines
}
req.session.destroy()
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
lines = req.body.lines
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, (error) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
res.send 200
req.session.destroy()

View file

@ -0,0 +1,25 @@
logger = require "logger-sharelatex"
Metrics = require "../../infrastructure/Metrics"
Project = require("../../models/Project").Project
ProjectZipStreamManager = require "./ProjectZipStreamManager"
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
module.exports = ProjectDownloadsController =
downloadProject: (req, res, next) ->
project_id = req.params.Project_id
Metrics.inc "zip-downloads"
logger.log project_id: project_id, "downloading project"
DocumentUpdaterHandler.flushProjectToMongo project_id, (error)->
return next(error) if error?
Project.findById project_id, "name", (error, project) ->
return next(error) if error?
ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) ->
return next(error) if error?
res.header(
"Content-Disposition",
"attachment; filename=#{encodeURIComponent(project.name)}.zip"
)
res.contentType('application/zip')
stream.pipe(res)

View file

@ -0,0 +1,46 @@
archiver = require "archiver"
async = require "async"
logger = require "logger-sharelatex"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
FileStoreHandler = require("../FileStore/FileStoreHandler")
module.exports = ProjectZipStreamManager =
createZipStreamForProject: (project_id, callback = (error, stream) ->) ->
archive = archiver("zip")
# return stream immediately before we start adding things to it
callback(null, archive)
@addAllDocsToArchive project_id, archive, (error) =>
if error?
logger.error err: error, project_id: project_id, "error adding docs to zip stream"
@addAllFilesToArchive project_id, archive, (error) =>
if error?
logger.error err: error, project_id: project_id, "error adding files to zip stream"
archive.finalize()
addAllDocsToArchive: (project_id, archive, callback = (error) ->) ->
ProjectEntityHandler.getAllDocs project_id, (error, docs) ->
return callback(error) if error?
jobs = []
for path, doc of docs
do (path, doc) ->
path = path.slice(1) if path[0] == "/"
jobs.push (callback) ->
logger.log project_id: project_id, "Adding doc"
archive.append doc.lines.join("\n"), name: path, callback
async.series jobs, callback
addAllFilesToArchive: (project_id, archive, callback = (error) ->) ->
ProjectEntityHandler.getAllFiles project_id, (error, files) ->
return callback(error) if error?
jobs = []
for path, file of files
do (path, file) ->
jobs.push (callback) ->
FileStoreHandler.getFileStream project_id, file._id, {}, (error, stream) ->
if error?
logger.err err:error, project_id:project_id, file_id:file._id, "something went wrong adding file to zip archive"
return callback(err)
path = path.slice(1) if path[0] == "/"
archive.append stream, name: path, callback
async.series jobs, callback

View file

@ -0,0 +1,81 @@
request = require('request')
settings = require('settings-sharelatex')
logger = require('logger-sharelatex')
Project = require('../../models/Project').Project
projectEntityHandler = require '../Project/ProjectEntityHandler'
_ = require('underscore')
async = require('async')
module.exports =
getUserRegistrationStatus: (user_id, callback)->
logger.log user_id:user_id, "getting dropbox registration status from tpds"
opts =
url : "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/status"
timeout: 5000
request.get opts, (err, response, body)->
safelyGetResponse err, response, body, (err, body)->
if err?
logger.err err:err, response:response, "getUserRegistrationStatus problem"
return callback err
logger.log status:body, "getting dropbox registration status for user #{user_id}"
callback err, body
getDropboxRegisterUrl: (user_id, callback)->
opts =
url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/register"
timeout: 5000
request.get opts, (err, response, body)->
safelyGetResponse err, response, body, (err, body)->
if err?
logger.err err:err, response:response, "getUserRegistrationStatus problem"
return callback err
url = "#{body.authorize_url}&oauth_callback=#{settings.siteUrl}/dropbox/completeRegistration"
logger.log user_id:user_id, url:url, "starting dropbox register"
callback err, url
completeRegistration: (user_id, callback)->
opts =
url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox/getaccesstoken"
timeout: 5000
request.get opts, (err, response, body)=>
safelyGetResponse err, response, body, (err, body)=>
if err?
logger.err err:err, response:response, "getUserRegistrationStatus problem"
return callback err
success = body.success
logger.log user_id:user_id, success:body.success, "completing dropbox register"
if success
@flushUsersProjectToDropbox user_id
callback err, body.success
unlinkAccount: (user_id, callback)->
opts =
url: "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/dropbox"
timeout: 5000
request.del opts, (err, response, body)=>
callback(err)
flushUsersProjectToDropbox: (user_id, callback)->
Project.findAllUsersProjects user_id, '_id', (projects = [], collabertions = [], readOnlyProjects = [])->
projectList = []
projectList = projectList.concat(projects)
projectList = projectList.concat(collabertions)
projectList = projectList.concat(readOnlyProjects)
projectIds = _.pluck(projectList, "_id")
logger.log projectIds:projectIds, user_id:user_id, "flushing all a users projects to tpds"
jobs = projectIds.map (project_id)->
return (cb)->
projectEntityHandler.flushProjectToThirdPartyDataStore project_id, cb
async.series jobs, callback
safelyGetResponse = (err, res, body, callback)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting response from dropbox, #{err}, #{statusCode}")
logger.err err:err
callback(e, [])
else
body = JSON.parse body
callback(null, body)

View file

@ -0,0 +1,245 @@
logger = require('logger-sharelatex')
Metrics = require('../../infrastructure/Metrics')
sanitize = require('validator').sanitize
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
ProjectEntityHandler = require('../Project/ProjectEntityHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectGetter = require('../Project/ProjectGetter')
ProjectHandler = new (require('../../handlers/ProjectHandler'))()
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
LimitationsManager = require("../Subscription/LimitationsManager")
AuthorizationManager = require("../Security/AuthorizationManager")
AutomaticSnapshotManager = require("../Versioning/AutomaticSnapshotManager")
VersioningApiHandler = require("../Versioning/VersioningApiHandler")
AnalyticsManager = require("../Analytics/AnalyticsManager")
EditorRealTimeController = require("./EditorRealTimeController")
settings = require('settings-sharelatex')
slReqIdHelper = require('soa-req-id')
tpdsPollingBackgroundTasks = require('../ThirdPartyDataStore/TpdsPollingBackgroundTasks')
async = require('async')
_ = require('underscore')
rclientPub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
rclientPub.auth(settings.redis.web.password)
rclientSub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
rclientSub.auth(settings.redis.web.password)
module.exports = EditorController =
protocolVersion: 2
reportError: (client, clientError, callback = () ->) ->
client.get "project_id", (error, project_id) ->
client.get "user_id", (error, user_id) ->
logger.error err: clientError, project_id: project_id, user_id: user_id, "client error"
callback()
joinProject: (client, user, project_id, callback) ->
logger.log user_id:user._id, project_id:project_id, "user joining project"
Metrics.inc "editor.join-project"
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
ProjectGetter.populateProjectWithUsers project, (error, project) ->
return callback(error) if error?
VersioningApiHandler.enableVersioning project, (error) ->
return callback(error) if error?
AuthorizationManager.getPrivilegeLevelForProject project, user,
(error, canAccess, privilegeLevel) ->
if error? or !canAccess
callback new Error("Not authorized")
else
AnalyticsManager.trackOpenEditor user, project
client.join(project_id)
client.set("project_id", project_id)
client.set("owner_id", project.owner_ref._id)
client.set("user_id", user._id)
client.set("first_name", user.first_name)
client.set("last_name", user.last_name)
client.set("email", user.email)
client.set("connected_time", new Date())
client.set("signup_date", user.signUpDate)
client.set("login_count", user.loginCount)
client.set("take_snapshots", project.existsInVersioningApi)
AuthorizationManager.setPrivilegeLevelOnClient client, privilegeLevel
callback null, ProjectEditorHandler.buildProjectModelView(project), privilegeLevel, EditorController.protocolVersion
leaveProject: (client, user) ->
self = @
client.get "project_id", (error, project_id) ->
return if error? or !project_id?
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientDisconnected", client.id)
logger.log user_id:user._id, project_id:project_id, "user leaving project"
self.flushProjectIfEmpty(project_id)
joinDoc: (client, project_id, doc_id, fromVersion, callback = (error, docLines, version) ->) ->
# fromVersion is optional
if typeof fromVersion == "function"
callback = fromVersion
fromVersion = -1
client.get "user_id", (error, user_id) ->
logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user joining doc"
Metrics.inc "editor.join-doc"
client.join doc_id
DocumentUpdaterHandler.getDocument project_id, doc_id, fromVersion, (err, docLines, version, ops)->
# Encode any binary bits of data so it can go via WebSockets
# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
if docLines?
docLines = for line in docLines
if line.text?
line.text = unescape(encodeURIComponent(line.text))
else
line = unescape(encodeURIComponent(line))
line
callback(err, docLines, version, ops)
leaveDoc: (client, project_id, doc_id, callback = (error) ->) ->
client.get "user_id", (error, user_id) ->
logger.log user_id: user_id, project_id: project_id, doc_id: doc_id, "user leaving doc"
Metrics.inc "editor.leave-doc"
client.leave doc_id
callback()
flushProjectIfEmpty: (project_id, callback = ->)->
setTimeout (->
io = require('../../infrastructure/Server').io
peopleStillInProject = io.sockets.clients(project_id).length
logger.log project_id: project_id, connectedCount: peopleStillInProject, "flushing if empty"
if peopleStillInProject == 0
DocumentUpdaterHandler.flushProjectToMongoAndDelete(project_id)
callback()
), 500
updateClientPosition: (client, cursorData, callback = (error) ->) ->
client.get "project_id", (error, project_id) ->
return callback(error) if error?
client.get "first_name", (error, first_name) ->
return callback(error) if error?
client.get "last_name", (error, last_name) ->
return callback(error) if error?
cursorData.id = client.id
if first_name? and last_name?
cursorData.name = first_name + " " + last_name
else
cursorData.name = "Anonymous"
EditorRealTimeController.emitToRoom(project_id, "clientTracking.clientUpdated", cursorData)
addUserToProject: (project_id, email, privlages, callback = (error, collaborator_added)->)->
email = email.toLowerCase()
LimitationsManager.isCollaboratorLimitReached project_id, (error, limit_reached) =>
if error?
logger.error err:error, "error adding user to to project when checking if collaborator limit has been reached"
return callback(new Error("Something went wrong"))
if limit_reached
callback null, false
else
ProjectHandler.addUserToProject project_id, email, privlages, (user)=>
EditorRealTimeController.emitToRoom(project_id, 'userAddedToProject', user, privlages)
callback null, true
removeUserFromProject: (project_id, user_id, callback)->
ProjectHandler.removeUserFromProject project_id, user_id, =>
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
if callback?
callback()
setCompiler : (project_id, compiler, callback = ()->)->
ProjectOptionsHandler.setCompiler project_id, compiler, (err)->
logger.log compiler:compiler, project_id:project_id, "setting compiler"
callback()
setSpellCheckLanguage : (project_id, languageCode, callback = ()->)->
ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err)->
logger.log languageCode:languageCode, project_id:project_id, "setting languageCode for spell check"
callback()
setDoc: (project_id, doc_id, docLines, sl_req_id, callback = (err)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
DocumentUpdaterHandler.setDocument project_id, doc_id, docLines, (err)=>
logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated"
EditorRealTimeController.emitToRoom(project_id, "entireDocUpdate", doc_id)
ProjectEntityHandler.updateDocLines project_id, doc_id, docLines, sl_req_id, (err)->
callback(err)
addDoc: (project_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
docName = sanitize(docName).xss()
logger.log sl_req_id:sl_req_id, "sending new doc to project #{project_id}"
Metrics.inc "editor.add-doc"
ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, sl_req_id, (err, doc, folder_id)=>
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc)
callback(err, doc)
addFile: (project_id, folder_id, fileName, path, sl_req_id, callback = (error, file)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
fileName = sanitize(fileName).xss()
logger.log sl_req_id:sl_req_id, "sending new file to project #{project_id} with folderid: #{folder_id}"
Metrics.inc "editor.add-file"
ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef)
callback(err, fileRef)
replaceFile: (project_id, file_id, fsPath, callback)->
ProjectEntityHandler.replaceFile project_id, file_id, fsPath, (err) ->
callback()
addFolder: (project_id, folder_id, folderName, sl_req_id, callback = (error, folder)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
folderName = sanitize(folderName).xss()
logger.log "sending new folder to project #{project_id}"
Metrics.inc "editor.add-folder"
ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
@p.notifyProjectUsersOfNewFolder project_id, folder_id, folder, (error) ->
callback error, folder
mkdirp: (project_id, path, callback)->
logger.log project_id:project_id, path:path, "making directories if they don't exist"
ProjectEntityHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=>
self = @
jobs = _.map newFolders, (folder, index)->
return (cb)->
self.p.notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb
async.series jobs, (err)->
callback err, newFolders, lastFolder
deleteEntity: (project_id, entity_id, entityType, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, entity_id:entity_id, entityType:entityType, "start delete process of entity"
Metrics.inc "editor.delete-entity"
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, =>
logger.log sl_req_id: sl_req_id, project_id:project_id, entity_id:entity_id, entityType:entityType, "telling users entity has been deleted"
EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id)
if callback?
callback()
getListOfDocPaths: (project_id, callback)->
ProjectEntityHandler.getAllDocs project_id, (err, docs)->
docList = _.map docs, (doc, path)->
return {_id:doc._id, path:path.substring(1)}
callback(null, docList)
forceResyncOfDropbox: (project_id, callback)->
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, callback
notifyUsersProjectHasBeenDeletedOrRenamed: (project_id, callback)->
EditorRealTimeController.emitToRoom(project_id, 'projectRenamedOrDeletedByExternalSource')
callback()
getLastTimePollHappned: (callback)->
tpdsPollingBackgroundTasks.getLastTimePollHappned callback
updateProjectDescription: (project_id, description, callback = ->)->
logger.log project_id:project_id, description:description, "updating project description"
ProjectDetailsHandler.setProjectDescription project_id, description, (err)->
if err?
logger.err err:err, project_id:project_id, description:description, "something went wrong setting the project description"
return callback(err)
EditorRealTimeController.emitToRoom(project_id, 'projectDescriptionUpdated', description)
callback()
p:
notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)->
logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users"
EditorRealTimeController.emitToRoom(project_id, "reciveNewFolder", folder_id, folder)
callback()

View file

@ -0,0 +1,31 @@
settings = require 'settings-sharelatex'
rclientPub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
rclientPub.auth(settings.redis.web.password)
rclientSub = require("redis").createClient(settings.redis.web.port, settings.redis.web.host)
rclientSub.auth(settings.redis.web.password)
module.exports = EditorRealTimeController =
rclientPub: rclientPub
rclientSub: rclientSub
emitToRoom: (room_id, message, payload...) ->
@rclientPub.publish "editor-events", JSON.stringify
room_id: room_id
message: message
payload: payload
emitToAll: (message, payload...) ->
@emitToRoom "all", message, payload...
listenForEditorEvents: () ->
@rclientSub.subscribe "editor-events"
@rclientSub.on "message", @_processEditorEvent.bind(@)
_processEditorEvent: (channel, message) ->
io = require('../../infrastructure/Server').io
message = JSON.parse(message)
if message.room_id == "all"
io.sockets.emit(message.message, message.payload...)
else
io.sockets.in(message.room_id).emit(message.message, message.payload...)

View file

@ -0,0 +1,68 @@
logger = require "logger-sharelatex"
metrics = require('../../infrastructure/Metrics')
Settings = require 'settings-sharelatex'
rclient = require("redis").createClient(Settings.redis.web.port, Settings.redis.web.host)
rclient.auth(Settings.redis.web.password)
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
AutomaticSnapshotManager = require("../Versioning/AutomaticSnapshotManager")
EditorRealTimeController = require("./EditorRealTimeController")
module.exports = EditorUpdatesController =
_applyUpdate: (client, project_id, doc_id, update, callback = (error) ->) ->
metrics.inc "editor.doc-update", 0.3
metrics.set "editor.active-projects", project_id, 0.3
client.get "user_id", (error, user_id) ->
metrics.set "editor.active-users", user_id, 0.3
client.get "take_snapshots", (error, takeSnapshot) ->
if takeSnapshot
AutomaticSnapshotManager.markProjectAsUpdated(project_id)
DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) ->
if error?
logger.error err:error, project_id: project_id, "document was not available for update"
client.disconnect()
callback(error)
applyOtUpdate: (client, project_id, doc_id, update) ->
update.meta ||= {}
update.meta.source = client.id
client.get "user_id", (error, user_id) ->
update.meta.user_id = user_id
EditorUpdatesController._applyUpdate client, project_id, doc_id, update
applyAceUpdate: (client, project_id, doc_id, window_name, update) ->
# This is deprecated now and should never be used. Kick the client off if they call it.
# After the initial deploy this can be removed safely
logger.err project_id: project_id, doc_id: doc_id, "client using old Ace Update method"
client.disconnect()
listenForUpdatesFromDocumentUpdater: () ->
rclient.subscribe "applied-ops"
rclient.on "message", @_processMessageFromDocumentUpdater.bind(@)
_processMessageFromDocumentUpdater: (channel, message) ->
message = JSON.parse message
if message.op?
@_applyUpdateFromDocumentUpdater(message.doc_id, message.op)
else if message.error?
@_processErrorFromDocumentUpdater(message.doc_id, message.error, message)
_applyUpdateFromDocumentUpdater: (doc_id, update) ->
io = require('../../infrastructure/Server').io
for client in io.sockets.clients(doc_id)
if client.id == update.meta.source
client.emit "otUpdateApplied", v: update.v, doc: update.doc
else
client.emit "otUpdateApplied", update
_processErrorFromDocumentUpdater: (doc_id, error, message) ->
io = require('../../infrastructure/Server').io
logger.error err: error, doc_id: doc_id, "error from document updater"
for client in io.sockets.clients(doc_id)
client.emit "otUpdateError", error, message
client.disconnect()

View file

@ -0,0 +1,61 @@
logger = require("logger-sharelatex")
fs = require("fs")
request = require("request")
settings = require("settings-sharelatex")
module.exports =
uploadFileFromDisk: (project_id, file_id, fsPath, callback)->
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
readStream = fs.createReadStream(fsPath)
opts =
method: "post"
uri: @_buildUrl(project_id, file_id)
writeStream = request(opts)
readStream.pipe writeStream
readStream.on "end", callback
readStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
callback err
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
getFileStream: (project_id, file_id, query, callback)->
logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store"
queryString = ""
if query? and query["format"]?
queryString = "?format=#{query['format']}"
opts =
method : "get"
uri: "#{@_buildUrl(project_id, file_id)}#{queryString}"
readStream = request(opts)
callback(null, readStream)
deleteFile: (project_id, file_id, callback)->
logger.log project_id:project_id, file_id:file_id, "telling file store to delete file"
opts =
method : "delete"
uri: @_buildUrl(project_id, file_id)
request opts, (err, response)->
if err?
logger.err err:err, project_id:project_id, file_id:file_id, "something went wrong deleting file from filestore"
callback(err)
copyFile: (oldProject_id, oldFile_id, newProject_id, newFile_id, callback)->
logger.log oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "telling filestore to copy a file"
opts =
method : "put"
json:
source:
project_id:oldProject_id
file_id:oldFile_id
uri: @_buildUrl(newProject_id, newFile_id)
request opts, (err)->
if err?
logger.err err:err, oldProject_id:oldProject_id, oldFile_id:oldFile_id, newProject_id:newProject_id, newFile_id:newFile_id, "something went wrong telling filestore api to copy file"
callback(err)
_buildUrl: (project_id, file_id)->
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"

View file

@ -0,0 +1,42 @@
Mocha = require "mocha"
Base = require("mocha/lib/reporters/base")
module.exports = HealthCheckController =
check: (req, res, next = (error) ->) ->
mocha = new Mocha(reporter: Reporter(res), timeout: 10000)
mocha.addFile("test/smoke/js/SmokeTests.js")
mocha.run () ->
path = require.resolve(__dirname + "/../../../../test/smoke/js/SmokeTests.js")
delete require.cache[path]
Reporter = (res) ->
(runner) ->
Base.call(this, runner)
tests = []
passes = []
failures = []
runner.on 'test end', (test) -> tests.push(test)
runner.on 'pass', (test) -> passes.push(test)
runner.on 'fail', (test) -> failures.push(test)
runner.on 'end', () =>
clean = (test) ->
title: test.fullTitle()
duration: test.duration
err: test.err
timedOut: test.timedOut
results = {
stats: @stats
failures: failures.map(clean)
passes: passes.map(clean)
}
res.contentType("application/json")
if failures.length > 0
res.send 500, JSON.stringify(results, null, 2)
else
res.send 200, JSON.stringify(results, null, 2)

View file

@ -0,0 +1,10 @@
_ = require "underscore"
module.exports =
areSame: (lines1, lines2)->
if !Array.isArray(lines1) or !Array.isArray(lines2)
return false
return _.isEqual(lines1, lines2)

View file

@ -0,0 +1,13 @@
ProjectDetailsHandler = require("./ProjectDetailsHandler")
logger = require("logger-sharelatex")
module.exports =
getProjectDetails : (req, res)->
{project_id} = req.params
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
res.json(projDetails)

View file

@ -0,0 +1,82 @@
logger = require('logger-sharelatex')
async = require("async")
metrics = require('../../infrastructure/Metrics')
Settings = require('settings-sharelatex')
ObjectId = require('mongoose').Types.ObjectId
Project = require('../../models/Project').Project
Folder = require('../../models/Folder').Folder
VersioningApiHandler = require('../Versioning/VersioningApiHandler')
ProjectEntityHandler = require('./ProjectEntityHandler')
User = require('../../models/User').User
fs = require('fs')
Path = require "path"
_ = require "underscore"
module.exports =
createBlankProject : (owner_id, projectName, callback = (error, project) ->)->
metrics.inc("project-creation")
logger.log owner_id:owner_id, projectName:projectName, "creating blank project"
rootFolder = new Folder {'name':'rootFolder'}
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
project.save (err)->
return callback(err) if err?
VersioningApiHandler.enableVersioning project._id, (err) ->
callback err, project
createBasicProject : (owner_id, projectName, callback = (error, project) ->)->
self = @
@createBlankProject owner_id, projectName, (error, project)->
return callback(error) if error?
self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)->
return callback(error) if error?
ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, "", (error, doc)->
return callback(error) if error?
ProjectEntityHandler.setRootDoc project._id, doc._id, (error) ->
callback(error, project)
createExampleProject: (owner_id, projectName, callback = (error, project) ->)->
self = @
@createBlankProject owner_id, projectName, (error, project)->
return callback(error) if error?
async.series [
(callback) ->
self._buildTemplate "main.tex", owner_id, projectName, (error, docLines)->
return callback(error) if error?
ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, "", (error, doc)->
return callback(error) if error?
ProjectEntityHandler.setRootDoc project._id, doc._id, callback
(callback) ->
self._buildTemplate "references.bib", owner_id, projectName, (error, docLines)->
return callback(error) if error?
ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "references.bib", docLines, "", (error, doc)->
callback(error)
(callback) ->
universePath = Path.resolve(__dirname + "/../../../templates/project_files/universe.jpg")
ProjectEntityHandler.addFile project._id, project.rootFolder[0]._id, "universe.jpg", universePath, callback
], (error) ->
callback(error, project)
_buildTemplate: (template_name, user_id, project_name, callback = (error, output) ->)->
User.findById user_id, "first_name last_name", (error, user)->
return callback(error) if error?
monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]
templatePath = Path.resolve(__dirname + "/../../../templates/project_files/#{template_name}")
fs.readFile templatePath, (error, template) ->
return callback(error) if error?
data =
project_name: project_name
user: user
year: new Date().getUTCFullYear()
month: monthNames[new Date().getUTCMonth()]
output = _.template(template.toString(), data)
callback null, output.split("\n")

View file

@ -0,0 +1,19 @@
Project = require('../../models/Project').Project
logger = require('logger-sharelatex')
editorController = require('../Editor/EditorController')
module.exports =
markAsDeletedByExternalSource : (project_id, callback)->
logger.log project_id:project_id, "marking project as deleted by external data source"
conditions = {_id:project_id}
update = {deletedByExternalDataSource:true}
Project.update conditions, update, {}, (err)->
editorController.notifyUsersProjectHasBeenDeletedOrRenamed project_id, ->
callback()
deleteUsersProjects: (owner_id, callback)->
logger.log owner_id:owner_id, "deleting users projects"
Project.remove owner_ref:owner_id, callback

View file

@ -0,0 +1,26 @@
ProjectGetter = require("./ProjectGetter")
Project = require('../../models/Project').Project
logger = require("logger-sharelatex")
module.exports =
getDetails: (project_id, callback)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
if err?
logger.err err:err, project_id:project_id, "error getting project"
return callback(err)
details =
name : project.name
description: project.description
compiler: project.compiler
logger.log project_id:project_id, details:details, "getting project details"
callback(err, details)
setProjectDescription: (project_id, description, callback)->
conditions = _id:project_id
update = description:description
logger.log conditions:conditions, update:update, project_id:project_id, description:description, "setting project description"
Project.update conditions, update, (err)->
if err?
logger.err err:err, "something went wrong setting project description"
callback(err)

View file

@ -0,0 +1,53 @@
projectCreationHandler = require('./ProjectCreationHandler')
projectEntityHandler = require('./ProjectEntityHandler')
projectLocator = require('./ProjectLocator')
projectOptionsHandler = require('./ProjectOptionsHandler')
DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler")
Project = require("../../models/Project").Project
_ = require('underscore')
async = require('async')
module.exports =
duplicate: (owner, originalProjectId, newProjectName, callback)->
DocumentUpdaterHandler.flushProjectToMongo originalProjectId, (err) ->
return callback(err) if err?
Project.findById originalProjectId, (err, originalProject) ->
return callback(err) if err?
projectCreationHandler.createBlankProject owner._id, newProjectName, (err, newProject)->
return callback(err) if err?
projectLocator.findRootDoc {project:originalProject}, (err, originalRootDoc)->
projectOptionsHandler.setCompiler newProject._id, originalProject.compiler
setRootDoc = _.once (doc_id)->
projectEntityHandler.setRootDoc newProject, doc_id
copyDocs = (originalFolder, newParentFolder, callback)->
jobs = originalFolder.docs.map (doc)->
return (callback)->
projectEntityHandler.addDoc newProject, newParentFolder._id, doc.name, doc.lines, (err, newDoc)->
if originalRootDoc? and newDoc.name == originalRootDoc.name
setRootDoc newDoc._id
callback()
async.series jobs, callback
copyFiles = (originalFolder, newParentFolder, callback)->
jobs = originalFolder.fileRefs.map (file)->
return (callback)->
projectEntityHandler.copyFileFromExistingProject newProject, newParentFolder._id, originalProject._id, file, callback
async.parallelLimit jobs, 5, callback
copyFolder = (folder, desFolder, callback)->
jobs = folder.folders.map (childFolder)->
return (callback)->
projectEntityHandler.addFolder newProject, desFolder._id, childFolder.name, (err, newFolder)->
copyFolder childFolder, newFolder, callback
jobs.push (cb)->
copyDocs folder, desFolder, cb
jobs.push (cb)->
copyFiles folder, desFolder, cb
async.series jobs, callback
copyFolder originalProject.rootFolder[0], newProject.rootFolder[0], ->
callback(err, newProject)

View file

@ -0,0 +1,63 @@
module.exports = ProjectEditorHandler =
buildProjectModelView: (project, options) ->
options ||= {}
if !options.includeUsers?
options.includeUsers = true
result =
_id : project._id
name : project.name
rootDoc_id : project.rootDoc_id
rootFolder : [@buildFolderModelView project.rootFolder[0]]
publicAccesLevel : project.publicAccesLevel
versioningVisible : !!project.existsInVersioningApi
dropboxEnabled : !!project.existsInDropbox
compiler : project.compiler
description: project.description
spellCheckLanguage: project.spellCheckLanguage
deletedByExternalDataSource : project.deletedByExternalDataSource || false
if options.includeUsers
result.features =
collaborators: -1 # Infinite
versioning: false
dropbox:false
if project.owner_ref.features?
if project.owner_ref.features.collaborators?
result.features.collaborators = project.owner_ref.features.collaborators
if project.owner_ref.features.versioning?
result.features.versioning = project.owner_ref.features.versioning
if project.owner_ref.features.dropbox?
result.features.dropbox = project.owner_ref.features.dropbox
result.owner = @buildUserModelView project.owner_ref, "owner"
result.members = []
for ref in project.readOnly_refs
result.members.push @buildUserModelView ref, "readOnly"
for ref in project.collaberator_refs
result.members.push @buildUserModelView ref, "readAndWrite"
return result
buildUserModelView: (user, privileges) ->
_id : user._id
first_name : user.first_name
last_name : user.last_name
email : user.email
privileges : privileges
signUpDate : user.signUpDate
buildFolderModelView: (folder) ->
_id : folder._id
name : folder.name
folders : @buildFolderModelView childFolder for childFolder in folder.folders
fileRefs : @buildFileModelView file for file in folder.fileRefs
docs : @buildDocModelView doc for doc in folder.docs
buildFileModelView: (file) ->
_id : file._id
name : file.name
buildDocModelView: (doc) ->
_id : doc._id
name : doc.name

View file

@ -0,0 +1,355 @@
Project = require('../../models/Project').Project
Doc = require('../../models/Doc').Doc
Folder = require('../../models/Folder').Folder
File = require('../../models/File').File
FileStoreHandler = require("../FileStore/FileStoreHandler")
Errors = require "../../errors"
tpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
projectLocator = require('./ProjectLocator')
path = require "path"
async = require "async"
_ = require('underscore')
logger = require('logger-sharelatex')
slReqIdHelper = require('soa-req-id')
docComparitor = require('./DocLinesComparitor')
projectUpdateHandler = require('./ProjectUpdateHandler')
module.exports = ProjectEntityHandler =
getAllFolders: (project_id, sl_req_id, callback) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log sl_req_id: sl_req_id, project_id:project_id, "getting all folders for project"
folders = {}
processFolder = (basePath, folder) ->
folders[basePath] = folder
processFolder path.join(basePath, childFolder.name), childFolder for childFolder in folder.folders
Project.findById project_id, (err, project) ->
return callback(err) if err?
return callback("no project") if !project?
processFolder "/", project.rootFolder[0]
callback null, folders
getAllDocs: (project_id, sl_req_id, callback) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, "getting all docs for project"
@getAllFolders project_id, sl_req_id, (err, folders) ->
return callback(err) if err?
docs = {}
for folderPath, folder of folders
for doc in folder.docs
docs[path.join(folderPath, doc.name)] = doc
logger.log count:_.keys(docs).length, project_id:project_id, "returning docs for project"
callback null, docs
getAllFiles: (project_id, sl_req_id, callback) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id:project_id, "getting all files for project"
@getAllFolders project_id, sl_req_id, (err, folders) ->
return callback(err) if err?
files = {}
for folderPath, folder of folders
for file in folder.fileRefs
files[path.join(folderPath, file.name)] = file
callback null, files
flushProjectToThirdPartyDataStore: (project_id, sl_req_id, callback) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
self = @
logger.log sl_req_id: sl_req_id, project_id:project_id, "flushing project to tpds"
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
documentUpdaterHandler.flushProjectToMongo project_id, undefined, (error) ->
Project.findById project_id, (err, project) ->
return callback(error) if error?
requests = []
self.getAllDocs project_id, (err, docs) ->
for docPath, doc of docs
do (docPath, doc) ->
requests.push (callback) ->
tpdsUpdateSender.addDoc {project_id:project_id, docLines:doc.lines, path:docPath, project_name:project.name, rev:doc.rev||0},
sl_req_id,
callback
self.getAllFiles project_id, (err, files) ->
for filePath, file of files
do (filePath, file) ->
requests.push (callback) ->
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev},
sl_req_id,
callback
async.series requests, (err) ->
logger.log sl_req_id: sl_req_id, project_id:project_id, "finished flushing project to tpds"
callback(err)
setRootDoc: (project_id, newRootDocID, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log sl_req_id: sl_req_id, project_id: project_id, rootDocId: newRootDocID, "setting root doc"
Project.update {_id:project_id}, {rootDoc_id:newRootDocID}, {}, callback
unsetRootDoc: (project_id, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log sl_req_id: sl_req_id, project_id: project_id, "removing root doc"
Project.update {_id:project_id}, {$unset: {rootDoc_id: true}}, {}, callback
addDoc: (project_or_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc, folder_id) ->)=>
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
Project.getProject project_or_id, "", (err, project) ->
logger.log sl_req_id: sl_req_id, project: project._id, folder_id: folder_id, doc_name: docName, "adding doc"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)=>
doc = new Doc name: docName, lines: docLines
Project.putElement project._id, folder_id, doc, "doc", (err, result)=>
tpdsUpdateSender.addDoc {project_id:project._id, docLines:docLines, path:result.path.fileSystem, project_name:project.name, rev:doc.rev}, sl_req_id, ->
callback(err, doc, folder_id)
addFile: (project_or_id, folder_id, fileName, path, sl_req_id, callback = (error, fileRef, folder_id) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
Project.getProject project_or_id, "", (err, project) ->
logger.log sl_req_id: sl_req_id, project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, project_name:project.name, rev:fileRef.rev}, "sl_req_id_here", ->
callback(err, fileRef, folder_id)
replaceFile: (project_or_id, file_id, fsPath, callback)->
Project.getProject project_or_id, "", (err, project) ->
findOpts =
project_id:project._id
element_id:file_id
type:"file"
projectLocator.findElement findOpts, (err, fileRef, path)=>
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, fsPath, (err)->
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:path.fileSystem, rev:fileRef.rev+1, project_name:project.name}, "sl_req_id_here", (error) ->
conditons = _id:project._id
inc = {}
inc["#{path.mongo}.rev"] = 1
set = {}
set["#{path.mongo}.created"] = new Date()
update =
"$inc": inc
"$set": set
Project.update conditons, update, {}, (err, second)->
callback()
copyFileFromExistingProject: (project_or_id, folder_id, originalProject_id, origonalFileRef, sl_req_id, callback = (error, fileRef, folder_id) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
Project.getProject project_or_id, "", (err, project) ->
logger.log sl_req_id: sl_req_id, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)=>
fileRef = new File name : origonalFileRef.name
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
if err?
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, sl_req_id, (error) ->
callback(error, fileRef, folder_id)
mkdirp: (project_or_id, path, sl_req_id, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
self = @
folders = path.split('/')
folders = _.select folders, (folder)->
return folder.length != 0
Project.getProject project_or_id, "", (err, project)=>
if path == '/'
logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder"
return callback(null, [], project.rootFolder[0])
logger.log project_id: project._id, path:path, folders:folders, "running mkdirp"
builtUpPath = ''
procesFolder = (previousFolders, folderName, callback)=>
previousFolders = previousFolders || []
parentFolder = previousFolders[previousFolders.length-1]
if parentFolder?
parentFolder_id = parentFolder._id
builtUpPath = "#{builtUpPath}/#{folderName}"
projectLocator.findElementByPath project_or_id, builtUpPath, (err, foundFolder)=>
if !foundFolder?
logger.log sl_req_id: sl_req_id, path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_or_id, parentFolder_id, folderName, sl_req_id, (err, newFolder, parentFolder_id)->
newFolder.parentFolder_id = parentFolder_id
previousFolders.push newFolder
callback null, previousFolders
else
foundFolder.filterOut = true
previousFolders.push foundFolder
callback null, previousFolders
async.reduce folders, [], procesFolder, (err, folders)->
lastFolder = folders[folders.length-1]
folders = _.select folders, (folder)->
!folder.filterOut
callback(null, folders, lastFolder)
addFolder: (project_or_id, parentFolder_id, folderName, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
folder = new Folder name: folderName
Project.getProject project_or_id, "", (err, project) ->
return callback(err) if err?
confirmFolder project, parentFolder_id, (parentFolder_id)=>
logger.log sl_req_id: sl_req_id, project: project_or_id, parentFolder_id:parentFolder_id, folderName:folderName, "new folder added"
Project.putElement project._id, parentFolder_id, folder, "folder", (err, result)=>
if callback?
callback(err, folder, parentFolder_id)
updateDocLines : (project_or_id, doc_id, docLines, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
Project.getProject project_or_id, "", (err, project)->
return callback(err) if err?
return callback(new Errors.NotFoundError("project not found")) if !project?
project_id = project._id
if err?
logger.err err:err,project_id:project_id, "error finding project"
callback err
else if !project?
logger.err project_id:project_id, doc_id:doc_id, err: new Error("project #{project_id} could not be found for doc #{doc_id}")
callback "could not find project #{project_id}"
else
projectLocator.findElement {project:project, element_id:doc_id, type:"docs"}, (err, doc, path)->
if err?
logger.err "error putting doc #{doc_id} in project #{project_id} #{err}"
callback err
else if docComparitor.areSame docLines, doc.lines
logger.log sl_req_id: sl_req_id, docLines:docLines, project_id:project_id, doc_id:doc_id, rev:doc.rev, "old doc lines are same as the new doc lines, not updating them"
callback()
else
logger.log sl_req_id: sl_req_id, project_id:project_id, doc_id:doc_id, docLines: docLines, oldDocLines: doc.lines, rev:doc.rev, "updating doc lines"
conditons = _id:project_id
update = {$set:{}, $inc:{}}
changeLines = {}
changeLines["#{path.mongo}.lines"] = docLines
inc = {}
inc["#{path.mongo}.rev"] = 1
update["$set"] = changeLines
update["$inc"] = inc
Project.update conditons, update, {}, (err, second)->
if(err)
logger.err(sl_req_id:sl_req_id, doc_id:doc_id, project_id:project_id, err:err, "error saving doc to mongo")
callback(err)
else
logger.log sl_req_id:sl_req_id, doc_id:doc_id, project_id:project_id, newDocLines:docLines, oldDocLines:doc.lines, "doc saved to mongo"
rev = doc.rev+1
projectUpdateHandler.markAsUpdated project_id
tpdsUpdateSender.addDoc {project_id:project_id, path:path.fileSystem, docLines:docLines, project_name:project.name, rev:rev}, sl_req_id, callback
moveEntity: (project_id, entity_id, folder_id, entityType, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
self = @
destinationFolder_id = folder_id
logger.log sl_req_id: sl_req_id, entityType:entityType, entity_id:entity_id, project_id:project_id, folder_id:folder_id, "moving entity"
if !entityType?
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)->
return callback(err) if err?
self._removeElementFromMongoArray Project, project_id, path.mongo, (err)->
return callback(err) if err?
Project.putElement project_id, destinationFolder_id, entity, entityType, (err, result)->
return callback(err) if err?
opts =
project_id:project_id
project_name:project.name
startPath:path.fileSystem
endPath:result.path.fileSystem,
rev:entity.rev
tpdsUpdateSender.moveEntity opts, sl_req_id, callback
deleteEntity: (project_id, entity_id, entityType, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
self = @
logger.log entity_id:entity_id, type:entityType, project_id:project_id, "deleting project entity"
if !entityType?
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
return callback(error) if error?
ProjectEntityHandler._cleanUpEntity project, entity, entityType, (error) ->
return callback(error) if error?
tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, sl_req_id, (error) ->
return callback(error) if error?
self._removeElementFromMongoArray Project, project_id, path.mongo, (error) ->
return callback(error) if error?
callback null
_cleanUpEntity: (project, entity, entityType, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
if(entityType.indexOf("file") != -1)
ProjectEntityHandler._cleanUpFile project, entity, sl_req_id, callback
else if (entityType.indexOf("doc") != -1)
ProjectEntityHandler._cleanUpDoc project, entity, sl_req_id, callback
else if (entityType.indexOf("folder") != -1)
ProjectEntityHandler._cleanUpFolder project, entity, sl_req_id, callback
else
callback()
_cleanUpDoc: (project, doc, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
project_id = project._id.toString()
doc_id = doc._id.toString()
unsetRootDocIfRequired = (callback) =>
if project.rootDoc_id? and project.rootDoc_id.toString() == doc_id
@unsetRootDoc project_id, callback
else
callback()
unsetRootDocIfRequired (error) ->
return callback(error) if error?
require('../../Features/DocumentUpdater/DocumentUpdaterHandler').deleteDoc project_id, doc_id, callback
_cleanUpFile: (project, file, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
project_id = project._id.toString()
file_id = file._id.toString()
FileStoreHandler.deleteFile project_id, file_id, callback
_cleanUpFolder: (project, folder, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
jobs = []
for doc in folder.docs
do (doc) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, sl_req_id, callback
for file in folder.fileRefs
do (file) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, sl_req_id, callback
for childFolder in folder.folders
do (childFolder) ->
jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, sl_req_id, callback
async.series jobs, callback
_removeElementFromMongoArray : (model, model_id, path, callback)->
conditons = {_id:model_id}
update = {"$unset":{}}
update["$unset"][path] = 1
model.update conditons, update, {}, (err)->
pullUpdate = {"$pull":{}}
nonArrayPath = path.slice(0, path.lastIndexOf("."))
pullUpdate["$pull"][nonArrayPath] = null
model.update conditons, pullUpdate, {}, (err)->
if callback?
callback(err)
confirmFolder = (project, folder_id, callback)->
logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project"
if folder_id+'' == 'undefined'
callback(project.rootFolder[0]._id)
else if folder_id != null
callback folder_id
else
callback(project.rootFolder[0]._id)

View file

@ -0,0 +1,61 @@
mongojs = require("../../infrastructure/mongojs")
db = mongojs.db
ObjectId = mongojs.ObjectId
async = require "async"
module.exports = ProjectGetter =
EXCLUDE_DEPTH: 8
getProjectWithoutDocLines: (project_id, callback=(error, project) ->) ->
excludes = {}
for i in [1..@EXCLUDE_DEPTH]
excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0
db.projects.find _id: ObjectId(project_id), excludes, (error, projects = []) ->
callback error, projects[0]
getProject: (query, projection, callback = (error, project) ->) ->
if typeof query == "string"
query = _id: ObjectId(query)
else if query instanceof ObjectId
query = _id: query
db.projects.findOne query, projection, callback
populateProjectWithUsers: (project, callback=(error, project) ->) ->
# eventually this should be in a UserGetter.getUser module
getUser = (user_id, callback=(error, user) ->) ->
unless user_id instanceof ObjectId
user_id = ObjectId(user_id)
db.users.find _id: user_id, (error, users = []) ->
callback error, users[0]
jobs = []
jobs.push (callback) ->
getUser project.owner_ref, (error, user) ->
return callback(error) if error?
if user?
project.owner_ref = user
callback null, project
readOnly_refs = project.readOnly_refs
project.readOnly_refs = []
for readOnly_ref in readOnly_refs
do (readOnly_ref) ->
jobs.push (callback) ->
getUser readOnly_ref, (error, user) ->
return callback(error) if error?
if user?
project.readOnly_refs.push user
callback null, project
collaberator_refs = project.collaberator_refs
project.collaberator_refs = []
for collaberator_ref in collaberator_refs
do (collaberator_ref) ->
jobs.push (callback) ->
getUser collaberator_ref, (error, user) ->
return callback(error) if error?
if user?
project.collaberator_refs.push user
callback null, project
async.series jobs, (error) -> callback error, project

View file

@ -0,0 +1,141 @@
Project = require('../../models/Project').Project
Errors = require "../../errors"
_ = require('underscore')
logger = require('logger-sharelatex')
async = require('async')
module.exports =
findElement: (options, callback = (err, element, path, parentFolder)->)->
{project, project_id, element_id, type} = options
elementType = sanitizeTypeOfElement type
count = 0
endOfBranch = ->
if --count == 0
logger.warn "element #{element_id} could not be found for project #{project_id || project._id}"
return callback(new Errors.NotFoundError("entity not found"))
search = (searchFolder, path)->
count++
element = _.find searchFolder[elementType], (el)-> el._id+'' == element_id+'' #need to ToString both id's for robustness
if !element? && searchFolder.folders? && searchFolder.folders.length != 0
_.each searchFolder.folders, (folder, index)->
newPath = {}
newPath[key] = value for own key,value of path #make a value copy of the string
newPath.fileSystem += "/#{folder.name}"
newPath.mongo += ".folders.#{index}"
search folder, newPath
endOfBranch()
return
else if element?
elementPlaceInArray = getIndexOf(searchFolder[elementType], element_id)
path.fileSystem += "/#{element.name}"
path.mongo +=".#{elementType}.#{elementPlaceInArray}"
callback(null, element, path, searchFolder)
else if !element?
return endOfBranch()
path = {fileSystem:'',mongo:'rootFolder.0'}
startSearch = (project)->
if element_id+'' == project.rootFolder[0]._id+''
callback(null, project.rootFolder[0], path, null)
else
search project.rootFolder[0], path
if project?
startSearch(project)
else
Project.findById project_id, (err, project)->
return callback(err) if err?
if !project?
return callback(new Errors.NotFoundError("project not found"))
startSearch project
findRootDoc : (opts, callback)->
getRootDoc = (project)=>
@findElement {project:project, element_id:project.rootDoc_id, type:"docs"}, callback
{project, project_id} = opts
if project?
getRootDoc project
else
Project.findById project_id, (err, project)->
getRootDoc project
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
getParentFolder = (haystackFolder, foldersList, level, cb)->
if foldersList.length == 0
return cb null, haystackFolder
needleFolderName = foldersList[level]
found = false
_.each haystackFolder.folders, (folder)->
if folder.name.toLowerCase() == needleFolderName.toLowerCase()
found = true
if level == foldersList.length-1
cb null, folder
else
getParentFolder(folder, foldersList, ++level, cb)
if !found
cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, folder #{foldersList[level]} could not be found")
getEntity = (folder, entityName, cb)->
if !entityName?
return cb null, folder
enteties = _.union folder.fileRefs, folder.docs, folder.folders
result = _.find enteties, (entity)->
entity.name.toLowerCase() == entityName.toLowerCase()
if result?
cb null, result
else
cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found")
Project.getProject project_or_id, "", (err, project)->
if needlePath == '' || needlePath == '/'
return callback(null, project.rootFolder[0])
if needlePath.indexOf('/') == 0
needlePath = needlePath.substring(1)
foldersList = needlePath.split('/')
needleName = foldersList.pop()
rootFolder = project.rootFolder[0]
logger.log project_id:project._id, path:needlePath, foldersList:foldersList, "looking for element by path"
jobs = new Array()
jobs.push(
(cb)->
getParentFolder rootFolder, foldersList, 0, cb
)
jobs.push(
(folder, cb)->
getEntity folder, needleName, cb
)
async.waterfall jobs, callback
findUsersProjectByName: (user_id, projectName, callback)->
Project.findAllUsersProjects user_id, 'name', (projects, collabertions=[])->
projects = projects.concat(collabertions)
projectName = projectName.toLowerCase()
project = _.find projects, (project)->
project.name.toLowerCase() == projectName
logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name"
callback(null, project)
sanitizeTypeOfElement = (elementType)->
lastChar = elementType.slice -1
if lastChar != "s"
elementType +="s"
if elementType == "files"
elementType = "fileRefs"
return elementType
getIndexOf = (searchEntity, id)->
length = searchEntity.length
count = 0
while(count < length)
if searchEntity[count]._id+"" == id+""
return count
count++

View file

@ -0,0 +1,38 @@
Project = require('../../models/Project').Project
logger = require('logger-sharelatex')
_ = require('underscore')
settings = require("settings-sharelatex")
safeCompilers = ["xelatex", "pdflatex", "latex", "lualatex"]
module.exports =
setCompiler : (project_id, compiler, callback = ()->)->
logger.log project_id:project_id, compiler:compiler, "setting the compiler"
compiler = compiler.toLowerCase()
if !_.contains safeCompilers, compiler
return callback()
conditions = {_id:project_id}
update = {compiler:compiler}
Project.update conditions, update, {}, (err)->
if callback?
callback()
setSpellCheckLanguage: (project_id, languageCode, callback = ()->)->
logger.log project_id:project_id, languageCode:languageCode, "setting the spell check language"
languageIsSafe = false
settings.languages.forEach (safeLang)->
if safeLang.code == languageCode
languageIsSafe = true
if languageCode == ""
languageIsSafe = true
if languageIsSafe
conditions = {_id:project_id}
update = {spellCheckLanguage:languageCode}
Project.update conditions, update, {}, (err)->
callback()
else
logger.err project_id:project_id, languageCode:languageCode, "tryed to set unsafe language"
callback()

View file

@ -0,0 +1,19 @@
slReqIdHelper = require('soa-req-id')
ProjectEntityHandler = require "./ProjectEntityHandler"
Path = require "path"
module.exports = ProjectRootDocManager =
setRootDocAutomatically: (project_id, sl_req_id, callback = (error) ->) ->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
ProjectEntityHandler.getAllDocs project_id, sl_req_id, (error, docs) ->
return callback(error) if error?
root_doc_id = null
for path, doc of docs
for line in doc.lines || []
if Path.extname(path).match(/\.R?tex$/) and line.match(/\\documentclass/)
root_doc_id = doc._id
if root_doc_id?
ProjectEntityHandler.setRootDoc project_id, root_doc_id, sl_req_id, callback
else
callback()

View file

@ -0,0 +1,10 @@
Project = require('../../models/Project').Project
logger = require('logger-sharelatex')
module.exports =
markAsUpdated : (project_id, callback)->
conditions = {_id:project_id}
update = {lastUpdated:Date.now()}
Project.update conditions, update, {}, (err)->
if callback?
callback()

View file

@ -0,0 +1,59 @@
logger = require('logger-sharelatex')
User = require('../../models/User').User
AnalyticsManager = require("../Analytics/AnalyticsManager")
SubscriptionLocator = require "../Subscription/SubscriptionLocator"
Settings = require "settings-sharelatex"
module.exports = ReferalAllocator =
allocate: (referal_id, new_user_id, referal_source, referal_medium, callback = ->)->
if !referal_id?
return logger.log new_user_id:new_user_id, "no referal for user"
logger.log referal_id:referal_id, new_user_id:new_user_id, "allocating users referal"
query = {"referal_id":referal_id}
User.findOne query, (error, user) ->
return callback(error) if error?
return callback(new Error("user not found")) if !user? or !user._id?
# Can be backgrounded
AnalyticsManager.trackReferral user, referal_source, referal_medium
if referal_source == "bonus"
User.update query, {
$push:
refered_users: new_user_id
$inc:
refered_user_count: 1
}, {}, (err)->
if err?
logger.err err:err, referal_id:referal_id, new_user_id:new_user_id, "something went wrong allocating referal"
return callback(err)
ReferalAllocator.assignBonus user._id, callback
else
callback()
assignBonus: (user_id, callback = (error) ->) ->
SubscriptionLocator.getUsersSubscription user_id, (error, subscription) ->
return callback(error) if error?
logger.log
subscription: subscription,
user_id: user_id,
"checking user doesn't have a subsciption before assigning bonus"
if !subscription? or !subscription.planCode?
query = _id: user_id
User.findOne query, (error, user) ->
return callback(error) if error
return callback(new Error("user not found")) if !user?
logger.log
user_id: user_id,
refered_user_count: user.refered_user_count,
bonus_features: Settings.bonus_features[user.refered_user_count],
"assigning bonus"
if user.refered_user_count? and Settings.bonus_features[user.refered_user_count]?
User.update query, { $set: features: Settings.bonus_features[user.refered_user_count] }, callback
else
callback()
else
callback()

View file

@ -0,0 +1,34 @@
module.exports =
use: (req, res, next)->
if req.query?
if req.query.referal?
req.session.referal_id = req.query.referal
else if req.query.r? # Short hand for referal
req.session.referal_id = req.query.r
else if req.query.fb_ref?
req.session.referal_id = req.query.fb_ref
if req.query.rm? # referal medium e.g. twitter, facebook, email
switch req.query.rm
when "fb"
req.session.referal_medium = "facebook"
when "t"
req.session.referal_medium = "twitter"
when "gp"
req.session.referal_medium = "google_plus"
when "e"
req.session.referal_medium = "email"
when "d"
req.session.referal_medium = "direct"
if req.query.rs? # referal source e.g. project share, bonus
switch req.query.rs
when "b"
req.session.referal_source = "bonus"
when "ps"
req.session.referal_source = "public_share"
when "ci"
req.session.referal_source = "collaborator_invite"
next()

View file

@ -0,0 +1,10 @@
logger = require('logger-sharelatex')
ReferalHandler = require('./ReferalHandler')
module.exports =
bonus: (req, res)->
ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)->
res.render "referal/bonus",
title: "Bonus - Please recommend us"
refered_users: refered_users
refered_user_count: (refered_users or []).length

View file

@ -0,0 +1,7 @@
User = require('../../models/User').User
module.exports =
getReferedUserIds: (user_id, callback)->
User.findById user_id, (err, user)->
refered_users = user.refered_users || []
callback "null", refered_users

View file

@ -0,0 +1,11 @@
User = require("../../models/User").User
module.exports = RefererMiddleware =
getUserReferalId: (req, res, next) ->
if req.session? and req.session.user?
User.findById req.session.user._id, (error, user) ->
return next(error) if error?
req.session.user.referal_id = user.referal_id
next()
else
next()

View file

@ -0,0 +1,38 @@
SecurityManager = require '../../managers/SecurityManager'
module.exports = AuthorizationManager =
getPrivilegeLevelForProject: (
project, user,
callback = (error, canAccess, privilegeLevel)->
) ->
# This is not tested because eventually this function should be brought into
# this module.
SecurityManager.userCanAccessProject user, project, (canAccess, privilegeLevel) ->
if canAccess
callback null, true, privilegeLevel
else
callback null, false
setPrivilegeLevelOnClient: (client, privilegeLevel) ->
client.set("privilege_level", privilegeLevel)
ensureClientCanViewProject: (client, callback = (error, project_id)->) ->
@ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite", "readOnly"], callback
ensureClientCanEditProject: (client, callback = (error, project_id)->) ->
@ensureClientHasPrivilegeLevelForProject client, ["owner", "readAndWrite"], callback
ensureClientCanAdminProject: (client, callback = (error, project_id)->) ->
@ensureClientHasPrivilegeLevelForProject client, ["owner"], callback
ensureClientHasPrivilegeLevelForProject: (client, levels, callback = (error, project_id)->) ->
client.get "privilege_level", (error, level) ->
return callback(error) if error?
if level?
client.get "project_id", (error, project_id) ->
return callback(error) if error?
if project_id?
if levels.indexOf(level) > -1
callback null, project_id

View file

@ -0,0 +1,24 @@
Settings = require('settings-sharelatex')
redis = require('redis')
rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host)
rclient.auth(Settings.redis.web.password)
buildKey = (k)->
return "LoginRateLimit:#{k}"
ONE_MIN = 60
ATTEMPT_LIMIT = 10
module.exports =
processLoginRequest: (email, callback)->
multi = rclient.multi()
multi.incr(buildKey(email))
multi.get(buildKey(email))
multi.expire(buildKey(email), ONE_MIN * 2)
multi.exec (err, results)->
loginCount = results[1]
allow = loginCount <= ATTEMPT_LIMIT
callback err, allow
recordSuccessfulLogin: (email, callback = ->)->
rclient.del buildKey(email), callback

View file

@ -0,0 +1,14 @@
request = require 'request'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
module.exports = SpellingController =
proxyRequestToSpellingApi: (req, res, next) ->
url = req.url.slice("/spelling".length)
url = "/user/#{req.session.user._id}#{url}"
req.headers["Host"] = Settings.apis.spelling.host
getReq = request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "Spelling API error"
res.send 500

View file

@ -0,0 +1,62 @@
logger = require("logger-sharelatex")
Project = require("../../models/Project").Project
User = require("../../models/User").User
SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex")
module.exports =
allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
getOwnerOfProject project_id, (error, owner)->
return callback(error) if error?
if owner.features? and owner.features.collaborators?
callback null, owner.features.collaborators
else
callback null, Settings.defaultPlanCode.collaborators
currentNumberOfCollaboratorsInProject: (project_id, callback) ->
Project.findById project_id, 'collaberator_refs readOnly_refs', (error, project) ->
return callback(error) if error?
callback null, (project.collaberator_refs.length + project.readOnly_refs.length)
isCollaboratorLimitReached: (project_id, callback = (error, limit_reached)->) ->
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
return callback(error) if error?
@currentNumberOfCollaboratorsInProject project_id, (error, current_number) =>
return callback(error) if error?
if current_number < allowed_number or allowed_number < 0
callback null, false
else
callback null, true
userHasSubscriptionOrFreeTrial: (user, callback = (err, hasSubscriptionOrTrial, subscription)->) ->
@userHasSubscription user, (err, hasSubscription, subscription)=>
@userHasFreeTrial user, (err, hasFreeTrial)=>
logger.log user_id:user._id, subscription:subscription, hasFreeTrial:hasFreeTrial, hasSubscription:hasSubscription, "checking if user has subscription or free trial"
callback null, hasFreeTrial or hasSubscription, subscription
userHasSubscription: (user, callback = (err, hasSubscription, subscription)->) ->
logger.log user_id:user._id, "checking if user has subscription"
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
logger.log user:user, subscription:subscription, "checking if user has subscription"
hasValidSubscription = subscription? and subscription.recurlySubscription_id? and subscription?.state != "expired"
callback err, hasValidSubscription, subscription
userHasFreeTrial: (user, callback = (err, hasFreeTrial, subscription)->) ->
logger.log user_id:user._id, "checking if user has free trial"
SubscriptionLocator.getUsersSubscription user, (err, subscription)->
callback err, subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?, subscription
hasGroupMembersLimitReached: (user_id, callback)->
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
limitReached = subscription.member_ids.length >= subscription.membersLimit
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
callback(null, limitReached)
getOwnerOfProject = (project_id, callback)->
Project.findById project_id, 'owner_ref', (error, project) ->
return callback(error) if error?
User.findById project.owner_ref, (error, owner) ->
callback(error, owner)

View file

@ -0,0 +1,9 @@
Settings = require("settings-sharelatex")
module.exports =
findLocalPlanInSettings: (planCode) ->
for plan in Settings.plans
return plan if plan.planCode == planCode
return null

View file

@ -0,0 +1,181 @@
querystring = require 'querystring'
crypto = require 'crypto'
request = require 'request'
Settings = require "settings-sharelatex"
xml2js = require "xml2js"
logger = require("logger-sharelatex")
module.exports = RecurlyWrapper =
apiUrl : "https://api.recurly.com/v2"
apiRequest : (options, callback) ->
options.url = @apiUrl + "/" + options.url
options.headers =
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
"Accept" : "application/xml"
"Content-Type" : "application/xml; charset=utf-8"
request options, (error, response, body) ->
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
error = "Recurly API returned with status code: #{response.statusCode}"
callback(error, response, body)
sign : (parameters, callback) ->
nestAttributesForQueryString = (attributes, base) ->
newAttributes = {}
for key, value of attributes
if base?
newKey = "#{base}[#{key}]"
else
newKey = key
if typeof value == "object"
for key, value of nestAttributesForQueryString(value, newKey)
newAttributes[key] = value
else
newAttributes[newKey] = value
return newAttributes
crypto.randomBytes 32, (error, buffer) ->
return callback error if error?
parameters.nonce = buffer.toString "base64"
parameters.timestamp = Math.round((new Date()).getTime() / 1000)
unsignedQuery = querystring.stringify nestAttributesForQueryString(parameters)
signed = crypto.createHmac("sha1", Settings.apis.recurly.privateKey).update(unsignedQuery).digest("hex")
signature = "#{signed}|#{unsignedQuery}"
callback null, signature
getSubscription: (subscriptionId, options, callback) ->
callback = options unless callback?
options ||= {}
if options.recurlyJsResult
url = "recurly_js/result/#{subscriptionId}"
else
url = "subscriptions/#{subscriptionId}"
@apiRequest({
url: url
}, (error, response, body) =>
return callback(error) if error?
@_parseSubscriptionXml body, (error, recurlySubscription) =>
return callback(error) if error?
if options.includeAccount
if recurlySubscription.account? and recurlySubscription.account.url?
accountId = recurlySubscription.account.url.match(/accounts\/(.*)/)[1]
else
return callback "I don't understand the response from Recurly"
@getAccount accountId, (error, account) ->
return callback(error) if error?
recurlySubscription.account = account
callback null, recurlySubscription
else
callback null, recurlySubscription
)
getAccount: (accountId, callback) ->
@apiRequest({
url: "accounts/#{accountId}"
}, (error, response, body) =>
return callback(error) if error?
@_parseAccountXml body, callback
)
updateSubscription: (subscriptionId, options, callback) ->
logger.log subscriptionId:subscriptionId, options:options, "telling recurly to update subscription"
requestBody = """
<subscription>
<plan_code>#{options.plan_code}</plan_code>
<timeframe>#{options.timeframe}</timeframe>
</subscription>
"""
@apiRequest({
url : "subscriptions/#{subscriptionId}"
method : "put"
body : requestBody
}, (error, response, responseBody) =>
return callback(error) if error?
@_parseSubscriptionXml responseBody, callback
)
cancelSubscription: (subscriptionId, callback) ->
logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
@apiRequest({
url: "subscriptions/#{subscriptionId}/cancel",
method: "put"
}, (error, response, body) ->
callback(error)
)
reactivateSubscription: (subscriptionId, callback) ->
logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
@apiRequest({
url: "subscriptions/#{subscriptionId}/reactivate",
method: "put"
}, (error, response, body) ->
callback(error)
)
_parseSubscriptionXml: (xml, callback) ->
@_parseXml xml, (error, data) ->
return callback(error) if error?
if data? and data.subscription?
recurlySubscription = data.subscription
else
return callback "I don't understand the response from Recurly"
callback null, recurlySubscription
_parseAccountXml: (xml, callback) ->
@_parseXml xml, (error, data) ->
return callback(error) if error?
if data? and data.account?
account = data.account
else
return callback "I don't understand the response from Recurly"
callback null, account
_parseXml: (xml, callback) ->
convertDataTypes = (data) ->
if data? and data["$"]?
if data["$"]["nil"] == "nil"
data = null
else if data["$"].href?
data.url = data["$"].href
delete data["$"]
else if data["$"]["type"] == "integer"
data = parseInt(data["_"], 10)
else if data["$"]["type"] == "datetime"
data = new Date(data["_"])
else if data["$"]["type"] == "array"
delete data["$"]
array = []
for key, value of data
if value instanceof Array
array = array.concat(convertDataTypes(value))
else
array.push(convertDataTypes(value))
data = array
if data instanceof Array
data = (convertDataTypes(entry) for entry in data)
else if typeof data == "object"
for key, value of data
data[key] = convertDataTypes(value)
return data
parser = new xml2js.Parser(
explicitRoot : true
explicitArray : false
)
parser.parseString xml, (error, data) ->
return callback(error) if error?
result = convertDataTypes(data)
callback null, result

View file

@ -0,0 +1,21 @@
async = require 'async'
logger = require 'logger-sharelatex'
SubscriptionUpdater = require("./SubscriptionUpdater")
SubscriptionLocator = require("./SubscriptionLocator")
AnalyticsManager = require("../Analytics/AnalyticsManager")
module.exports = SubscriptionBackgroundJobs =
# TODO: Remove this one month after the ability to start free trials was removed
downgradeExpiredFreeTrials: (callback = (error, subscriptions)->) ->
SubscriptionLocator.expiredFreeTrials (error, subscriptions) =>
return callback(error) if error?
logger.log total_subscriptions:subscriptions.length, "downgraging subscriptions"
downgrades = []
for subscription in subscriptions
do (subscription) =>
downgrades.push (cb) =>
logger.log subscription: subscription, "downgrading free trial"
AnalyticsManager.trackFreeTrialExpired subscription.admin_id
SubscriptionUpdater.downgradeFreeTrial(subscription, cb)
async.series downgrades, (error) -> callback(error, subscriptions)

View file

@ -0,0 +1,169 @@
SecurityManager = require '../../managers/SecurityManager'
SubscriptionHandler = require './SubscriptionHandler'
PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
LimitationsManager = require("./LimitationsManager")
RecurlyWrapper = require './RecurlyWrapper'
Settings = require 'settings-sharelatex'
logger = require('logger-sharelatex')
module.exports = SubscriptionController =
plansPage: (req, res, next) ->
plans = SubscriptionViewModelBuilder.buildViewModel()
if !req.session.user?
for plan in plans
plan.href = "/register?redir=#{plan.href}"
viewName = "subscriptions/plans"
if req.query.variant?
viewName += req.query.variant
logger.log viewName:viewName, "showing plans page"
res.render viewName,
title: "Plans and Pricing"
plans: plans
#get to show the recurly.js page
paymentPage: (req, res, next) ->
SecurityManager.getCurrentUser req, (error, user) =>
return next(error) if error?
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
if hasSubscription or !plan?
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
subscription:
plan_code : req.query.planCode
account_code: user.id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/new",
title : "Subscribe"
plan_code: req.query.planCode
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
subscriptionFormOptions: JSON.stringify
acceptedCards: ['discover', 'mastercard', 'visa']
target : "#subscribeForm"
signature : signature
planCode : req.query.planCode
successURL : "#{Settings.siteUrl}/user/subscription/create?_csrf=#{req.session._csrf}"
accountCode : user.id
enableCoupons: true
acceptPaypal: true
account :
firstName : user.first_name
lastName : user.last_name
email : user.email
userSubscriptionPage: (req, res, next) ->
SecurityManager.getCurrentUser req, (error, user) =>
return next(error) if error?
LimitationsManager.userHasSubscriptionOrFreeTrial user, (err, hasSubOrFreeTrial)->
if !hasSubOrFreeTrial
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrFreeTrial:hasSubOrFreeTrial, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "Your Subscription"
plans: plans
subscription: subscription
subscriptionTabActive: true
editBillingDetailsPage: (req, res, next) ->
SecurityManager.getCurrentUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user.id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "Update Billing Details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/update"
user :
id : user.id
createSubscription: (req, res, next)->
SecurityManager.getCurrentUser req, (error, user) ->
return callback(error) if error?
subscriptionId = req.body.recurly_token
logger.log subscription_id: subscriptionId, user_id:user._id, "creating subscription"
SubscriptionHandler.createSubscription user, subscriptionId, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
res.redirect "/user/subscription/thank-you"
successful_subscription: (req, res)->
SecurityManager.getCurrentUser req, (error, user) =>
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
res.render "subscriptions/successful_subscription",
title: "Thank you!"
subscription:subscription
cancelSubscription: (req, res, next) ->
SecurityManager.getCurrentUser req, (error, user) ->
logger.log user_id:user._id, "canceling subscription"
return next(error) if error?
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
res.redirect "/user/subscription"
updateSubscription: (req, res)->
SecurityManager.getCurrentUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
res.redirect "/user/subscription"
reactivateSubscription: (req, res)->
SecurityManager.getCurrentUser req, (error, user) ->
logger.log user_id:user._id, "reactivating subscription"
return next(error) if error?
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
res.redirect "/user/subscription"
recurlyCallback: (req, res)->
logger.log data: req.body, "received recurly callback"
# we only care if a subscription has exipired
if req.body? and req.body["expired_subscription_notification"]?
recurlySubscription = req.body["expired_subscription_notification"].subscription
SubscriptionHandler.recurlyCallback recurlySubscription, ->
res.send 200
else
res.send 200
recurlyNotificationParser: (req, res, next) ->
xml = ""
req.on "data", (chunk) ->
xml += chunk
req.on "end", () ->
RecurlyWrapper._parseXml xml, (error, body) ->
return next(error) if error?
req.body = body
next()

View file

@ -0,0 +1,15 @@
dateformat = require 'dateformat'
module.exports =
formatPrice: (priceInCents) ->
string = priceInCents + ""
string = "0" + string if string.length == 2
string = "00" + string if string.length == 1
string = "000" if string.length == 0
cents = string.slice(-2)
dollars = string.slice(0, -2)
return "$#{dollars}.#{cents}"
formatDate: (date) ->
dateformat date, "dS mmmm yyyy"

View file

@ -0,0 +1,34 @@
SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator")
module.exports =
addUserToGroup: (req, res)->
adminUserId = req.session.user._id
newEmail = req.body.email
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
result =
user:user
if err and err.limitReached
result.limitReached = true
res.json(result)
removeUserFromGroup: (req, res)->
adminUserId = req.session.user._id
userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, ->
res.send()
renderSubscriptionGroupAdminPage: (req, res)->
user_id = req.session.user._id
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
return res.redirect("/")
SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)->
res.render "subscriptions/group_admin",
title: 'Group Admin'
users: users
subscription: subscription

View file

@ -0,0 +1,45 @@
async = require("async")
_ = require("underscore")
UserCreator = require("../User/UserCreator")
SubscriptionUpdater = require("./SubscriptionUpdater")
SubscriptionLocator = require("./SubscriptionLocator")
UserLocator = require("../User/UserLocator")
LimitationsManager = require("./LimitationsManager")
module.exports =
addUserToGroup: (adminUser_id, newEmail, callback)->
UserCreator.getUserOrCreateHoldingAccount newEmail, (err, user)->
LimitationsManager.hasGroupMembersLimitReached adminUser_id, (err, limitReached)->
if limitReached
return callback(limitReached:limitReached)
SubscriptionUpdater.addUserToGroup adminUser_id, user._id, (err)->
userViewModel = buildUserViewModel(user)
callback(err, userViewModel)
removeUserFromGroup: (adminUser_id, userToRemove_id, callback)->
SubscriptionUpdater.removeUserFromGroup adminUser_id, userToRemove_id, callback
getPopulatedListOfMembers: (adminUser_id, callback)->
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
users = []
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserLocator.findById user_id, (err, user)->
userViewModel = buildUserViewModel(user)
users.push(userViewModel)
cb()
async.series jobs, (err)->
callback(err, users)
buildUserViewModel = (user)->
u =
email: user.email
first_name: user.first_name
last_name: user.last_name
holdingAccount: user.holdingAccount
_id: user._id
return u

View file

@ -0,0 +1,58 @@
RecurlyWrapper = require("./RecurlyWrapper")
Settings = require "settings-sharelatex"
User = require('../../models/User').User
logger = require('logger-sharelatex')
AnalyticsManager = require '../../Features/Analytics/AnalyticsManager'
SubscriptionUpdater = require("./SubscriptionUpdater")
LimitationsManager = require('./LimitationsManager')
module.exports =
createSubscription: (user, recurlySubscriptionId, callback)->
self = @
RecurlyWrapper.getSubscription recurlySubscriptionId, {recurlyJsResult: true}, (error, recurlySubscription) ->
return callback(error) if error?
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) ->
return callback(error) if error?
AnalyticsManager.trackSubscriptionStarted user, recurlySubscription?.plan?.plan_code
callback()
updateSubscription: (user, plan_code, callback)->
logger.log user:user, plan_code:plan_code, "updating subscription"
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
if hasSubscription
RecurlyWrapper.updateSubscription subscription.recurlySubscription_id, {plan_code: plan_code, timeframe: "now"}, (error, recurlySubscription) ->
return callback(error) if error?
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, callback
else
callback()
cancelSubscription: (user, callback) ->
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
if hasSubscription
RecurlyWrapper.cancelSubscription subscription.recurlySubscription_id, (error) ->
return callback(error) if error?
AnalyticsManager.trackSubscriptionCancelled user
callback()
else
callback()
reactivateSubscription: (user, callback) ->
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
if hasSubscription
RecurlyWrapper.reactivateSubscription subscription.recurlySubscription_id, (error) ->
return callback(error) if error?
callback()
else
callback()
recurlyCallback: (recurlySubscription, callback) ->
RecurlyWrapper.getSubscription recurlySubscription.uuid, includeAccount: true, (error, recurlySubscription) ->
return callback(error) if error?
User.findById recurlySubscription.account.account_code, (error, user) ->
return callback(error) if error?
SubscriptionUpdater.syncSubscription recurlySubscription, user._id, callback

View file

@ -0,0 +1,20 @@
Subscription = require('../../models/Subscription').Subscription
logger = require("logger-sharelatex")
ObjectId = require('mongoose').Types.ObjectId
module.exports =
getUsersSubscription: (user_or_id, callback)->
if user_or_id? and user_or_id._id?
user_id = user_or_id._id
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
# TODO: Remove this one month after the ability to start free trials was removed
expiredFreeTrials: (callback = (error, subscriptions)->) ->
query =
"freeTrial.expiresAt": "$lt": new Date()
"freeTrial.downgraded": "$ne": true
Subscription.find query, callback

View file

@ -0,0 +1,36 @@
AuthenticationController = require('../Authentication/AuthenticationController')
SubscriptionController = require('./SubscriptionController')
SubscriptionGroupController = require './SubscriptionGroupController'
Settings = require "settings-sharelatex"
module.exports =
apply: (app) ->
return unless Settings.enableSubscriptions
app.get '/user/subscription/plans', SubscriptionController.plansPage
app.get '/user/subscription', AuthenticationController.requireLogin(), SubscriptionController.userSubscriptionPage
app.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
app.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
app.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.del '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
#recurly callback
app.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback
app.ignoreCsrf("post", '/user/subscription/callback')
#user changes there 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

View file

@ -0,0 +1,80 @@
async = require("async")
_ = require("underscore")
Subscription = require('../../models/Subscription').Subscription
SubscriptionLocator = require("./SubscriptionLocator")
UserFeaturesUpdater = require("./UserFeaturesUpdater")
PlansLocator = require("./PlansLocator")
Settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
ObjectId = require('mongoose').Types.ObjectId
oneMonthInSeconds = 60 * 60 * 24 * 30
module.exports =
syncSubscription: (recurlySubscription, adminUser_id, callback) ->
self = @
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "syncSubscription, creating new if subscription does not exist"
SubscriptionLocator.getUsersSubscription adminUser_id, (err, subscription)->
if subscription?
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does exist"
self._updateSubscription recurlySubscription, subscription, callback
else
logger.log adminUser_id:adminUser_id, recurlySubscription:recurlySubscription, "subscription does not exist, creating a new one"
self._createNewSubscription adminUser_id, (err, subscription)->
self._updateSubscription recurlySubscription, subscription, callback
# TODO: Remove this one month after the ability to start free trials was removed
downgradeFreeTrial: (subscription, callback = (error)->) ->
UserFeaturesUpdater.updateFeatures subscription.admin_id, Settings.defaultPlanCode, ->
subscription.freeTrial.downgraded = true
subscription.save callback
addUserToGroup: (adminUser_id, user_id, callback)->
logger.log adminUser_id:adminUser_id, user_id:user_id, "adding user into mongo subscription"
searchOps =
admin_id: adminUser_id
insertOperation =
"$addToSet": {member_ids:user_id}
Subscription.findAndModify searchOps, insertOperation, (err, subscription)->
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
removeUserFromGroup: (adminUser_id, user_id, callback)->
searchOps =
admin_id: adminUser_id
removeOperation =
"$pull": {member_ids:user_id}
Subscription.update searchOps, removeOperation, ->
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback
_createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription"
subscription = new Subscription(admin_id:adminUser_id)
subscription.freeTrial.allowed = false
subscription.save (err)->
callback err, subscription
_updateSubscription: (recurlySubscription, subscription, callback)->
logger.log recurlySubscription:recurlySubscription, subscription:subscription, "updaing subscription"
plan = PlansLocator.findLocalPlanInSettings(recurlySubscription.plan.plan_code)
if recurlySubscription.state == "expired"
subscription.recurlySubscription_id = undefined
subscription.planCode = Settings.defaultPlanCode
else
subscription.recurlySubscription_id = recurlySubscription.uuid
subscription.freeTrial.expiresAt = undefined
subscription.freeTrial.planCode = undefined
subscription.freeTrial.allowed = true
subscription.planCode = recurlySubscription.plan.plan_code
if plan.groupPlan
subscription.groupPlan = true
subscription.membersLimit = plan.membersLimit
subscription.save ->
allIds = _.union subscription.members_id, [subscription.admin_id]
jobs = allIds.map (user_id)->
return (cb)->
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, cb
async.parallel jobs, callback

View file

@ -0,0 +1,63 @@
Settings = require('settings-sharelatex')
RecurlyWrapper = require("./RecurlyWrapper")
PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
LimitationsManager = require("./LimitationsManager")
SubscriptionLocator = require("./SubscriptionLocator")
_ = require("underscore")
module.exports =
buildUsersSubscriptionViewModel: (user, callback) ->
SubscriptionLocator.getUsersSubscription user, (err, subscription)->
LimitationsManager.userHasFreeTrial user, (err, hasFreeTrial)->
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
if hasSubscription
return callback(error) if error?
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
callback null,
name: plan.name
nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription.current_period_ends_at)
state: recurlySubscription.state
price: SubscriptionFormatters.formatPrice recurlySubscription.unit_amount_in_cents
planCode: subscription.planCode
groupPlan: subscription.groupPlan
else if hasFreeTrial
plan = PlansLocator.findLocalPlanInSettings(subscription.freeTrial.planCode)
callback null,
name: plan.name
state: "free-trial"
planCode: plan.planCode
groupPlan: subscription.groupPlan
expiresAt: SubscriptionFormatters.formatDate(subscription.freeTrial.expiresAt)
else
callback "User has no subscription"
buildViewModel : ->
plans = Settings.plans
result =
allPlans: plans
result.personalAccount = _.find plans, (plan)->
plan.planCode == "personal"
result.studentAccounts = _.filter plans, (plan)->
plan.planCode.indexOf("student") != -1
result.groupMonthlyPlans = _.filter plans, (plan)->
plan.groupPlan and !plan.annual
result.groupAnnualPlans = _.filter plans, (plan)->
plan.groupPlan and plan.annual
result.individualMonthlyPlans = _.filter plans, (plan)->
!plan.groupPlan and !plan.annual and plan.planCode != "personal" and plan.planCode.indexOf("student") == -1
result.individualAnnualPlans = _.filter plans, (plan)->
!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
return result

View file

@ -0,0 +1,16 @@
Settings = require "settings-sharelatex"
logger = require("logger-sharelatex")
User = require('../../models/User').User
PlansLocator = require("./PlansLocator")
module.exports =
updateFeatures: (user_id, plan_code, callback = (err, features)->)->
conditions = _id:user_id
update = {}
plan = PlansLocator.findLocalPlanInSettings(plan_code)
logger.log user_id:user_id, plan:plan, plan_code:plan_code, "updating users features"
update["features.#{key}"] = value for key, value of plan.features
User.update conditions, update, (err)->
callback err, plan.features

View file

@ -0,0 +1,21 @@
TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex")
module.exports =
processTagsUpdate: (req, res)->
user_id = req.session.user._id
project_id = req.params.project_id
if req.body.deletedTag?
tag = req.body.deletedTag
TagsHandler.deleteTag user_id, project_id, tag, ->
res.send()
else
tag = req.body.tag
TagsHandler.addTag user_id, project_id, tag, ->
res.send()
logger.log user_id:user_id, project_id:project_id, body:req.body, "processing tag update"
getAllTags: (req, res)->
TagsHandler.getAllTags req.session.user._id, (err, allTags)->
res.send(allTags)

View file

@ -0,0 +1,71 @@
_ = require('underscore')
settings = require("settings-sharelatex")
request = require("request")
logger = require("logger-sharelatex")
module.exports =
deleteTag: (user_id, project_id, tag, callback)->
uri = buildUri(user_id, project_id)
opts =
uri:uri
json:
name:tag
logger.log user_id:user_id, project_id:project_id, tag:tag, "send delete tag to tags api"
request.del opts, callback
addTag: (user_id, project_id, tag, callback)->
uri = buildUri(user_id, project_id)
opts =
uri:uri
json:
name:tag
logger.log user_id:user_id, project_id:project_id, tag:tag, "send add tag to tags api"
request.post opts, callback
requestTags: (user_id, callback)->
opts =
uri: "#{settings.apis.tags.url}/user/#{user_id}/tag"
json: true
timeout: 2000
request.get opts, (err, res, body)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting tags, #{err}, #{statusCode}")
logger.err err:err
callback(e, [])
else
callback(null, body)
getAllTags: (user_id, callback)->
@requestTags user_id, (err, allTags)=>
if !allTags?
allTags = []
@groupTagsByProject allTags, (err, groupedByProject)->
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "getting all tags from tags api"
callback err, allTags, groupedByProject
removeProjectFromAllTags: (user_id, project_id, callback)->
uri = buildUri(user_id, project_id)
opts =
uri:"#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
logger.log user_id:user_id, project_id:project_id, "removing project_id from tags"
request.del opts, callback
groupTagsByProject: (tags, callback)->
result = {}
_.each tags, (tag)->
_.each tag.project_ids, (project_id)->
result[project_id] = []
_.each tags, (tag)->
_.each tag.project_ids, (project_id)->
clonedTag = _.clone(tag)
delete clonedTag.project_ids
result[project_id].push(clonedTag)
callback null, result
buildUri = (user_id, project_id)->
uri = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}/tag"

View file

@ -0,0 +1,44 @@
path = require('path')
ProjectUploadManager = require('../Uploads/ProjectUploadManager')
ProjectOptionsHandler = require("../Project/ProjectOptionsHandler")
TemplatesPublisher = require("./TemplatesPublisher")
settings = require('settings-sharelatex')
fs = require('fs')
request = require('request')
uuid = require('node-uuid')
logger = require('logger-sharelatex')
module.exports =
createProjectFromZipTemplate: (req, res)->
logger.log body:req.session.templateData, "creating project from zip"
if !req.session.templateData?
return res.redirect "/project"
dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}"
writeStream = fs.createWriteStream(dumpPath)
zipUrl = req.session.templateData.zipUrl
if zipUrl.indexOf("www") == -1
zipUrl = "www.sharelatex.com#{zipUrl}"
request("http://#{zipUrl}").pipe(writeStream)
writeStream.on 'close', ->
ProjectUploadManager.createProjectFromZipArchive req.session.user._id, req.session.templateData.templateName, dumpPath, (err, project)->
setCompiler project._id, req.session.templateData.compiler, ->
fs.unlink dumpPath, ->
delete req.session.templateData
res.redirect "/project/#{project._id}"
publishProject: (user_id, project_id, callback)->
logger.log user_id:user_id, project_id:project_id, "reciving request to publish project as template"
TemplatesPublisher.publish user_id, project_id, callback
unPublishProject: (user_id, project_id, callback)->
logger.log user_id:user_id, project_id:project_id, "reciving request to unpublish project as template"
TemplatesPublisher.unpublish user_id, project_id, callback
setCompiler = (project_id, compiler, callback)->
if compiler?
ProjectOptionsHandler.setCompiler project_id, compiler, callback
else
callback()

View file

@ -0,0 +1,7 @@
module.exports =
saveTemplateDataInSession: (req, res, next)->
if req.query.templateName
req.session.templateData = req.query
next()

View file

@ -0,0 +1,21 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
publish : (user_id, project_id, callback)->
url = buildUrl(user_id, project_id)
request.post url, (err)->
if err?
logger.err err:err, "something went wrong publishing project as template"
callback err
unpublish: (user_id, project_id, callback)->
url = buildUrl(user_id, project_id)
request.del url, (err)->
callback()
buildUrl = (user_id, project_id)->
url = "#{settings.apis.templates_api.url}/templates-api/user/#{user_id}/project/#{project_id}"

View file

@ -0,0 +1,49 @@
tpdsUpdateHandler = require('./TpdsUpdateHandler')
logger = require('logger-sharelatex')
Path = require('path')
metrics = require("../../infrastructure/Metrics")
module.exports =
mergeUpdate: (req, res)->
metrics.inc("tpds.merge-update")
{filePath, user_id, projectName} = parseParams(req)
logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], projectName:projectName, sl_req_id:req.sl_req_id, "reciving update request from tpds"
tpdsUpdateHandler.newUpdate user_id, projectName, filePath, req, req.sl_req_id, (err)->
logger.log user_id:user_id, filePath:filePath, fullPath:req.params[0], sl_req_id:req.sl_req_id, "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)
else
logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds update has been processed"
res.send 200
req.session.destroy()
deleteUpdate: (req, res)->
metrics.inc("tpds.delete-update")
{filePath, user_id, projectName} = parseParams(req)
logger.log user_id:user_id, filePath:filePath, sl_req_id:req.sl_req_id, projectName:projectName, fullPath:req.params[0], "reciving delete request from tpds"
tpdsUpdateHandler.deleteUpdate user_id, projectName, filePath, req.sl_req_id, (err)->
if err?
logger.err err:err, user_id:user_id, filePath:filePath, "error reciving update from tpds"
res.send(500)
else
logger.log user_id:user_id, filePath:filePath, projectName:projectName, "telling tpds delete has been processed"
res.send 200
req.session.destroy()
parseParams: parseParams = (req)->
path = req.params[0]
user_id = req.params.user_id
path = Path.join("/",path)
if path.substring(1).indexOf('/') == -1
filePath = "/"
projectName = path.substring(1)
else
filePath = path.substring(path.indexOf("/",1))
projectName = path.substring(0, path.indexOf("/",1))
projectName = projectName.replace("/","")
return filePath:filePath, user_id:user_id, projectName:projectName

View file

@ -0,0 +1,35 @@
User = require('../../models/User').User
settings = require('settings-sharelatex')
request = require "request"
logger = require('logger-sharelatex')
redis = require('redis')
rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host)
rclient.auth(settings.redis.web.password)
LAST_TIME_POLL_HAPPEND_KEY = "LAST_TIME_POLL_HAPPEND_KEY"
self = module.exports =
pollUsersWithDropbox: (callback)->
self._getUserIdsWithDropbox (err, user_ids)=>
logger.log user_ids:user_ids, userCount:user_ids.length, "telling tpds to poll users with dropbox"
self._markPollHappened()
self._sendToTpds user_ids, callback
_sendToTpds : (user_ids, callback)->
if user_ids.length > 0
request.post {uri:"#{settings.apis.thirdPartyDataStore.url}/user/poll", json:{user_ids:user_ids}}, callback
else if callback?
callback()
_getUserIdsWithDropbox: (callback)->
User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id", (err, users)->
ids = users.map (user)->
return user._id+""
callback err, ids
_markPollHappened: (callback)->
rclient.set LAST_TIME_POLL_HAPPEND_KEY, new Date().getTime(), callback
getLastTimePollHappned: (callback = (err, lastTimePollHappened)->)->
rclient.get LAST_TIME_POLL_HAPPEND_KEY, callback

View file

@ -0,0 +1,46 @@
versioningApiHandler = require('../Versioning/VersioningApiHandler')
updateMerger = require('./UpdateMerger')
logger = require('logger-sharelatex')
projectLocator = require('../Project/ProjectLocator')
projectCreationHandler = require('../Project/ProjectCreationHandler')
projectDeleter = require('../Project/ProjectDeleter')
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
commitMessage = "Before update from Dropbox"
module.exports =
newUpdate: (user_id, projectName, path, updateRequest, sl_req_id, callback)->
getOrCreateProject = (cb)=>
projectLocator.findUsersProjectByName user_id, projectName, (err, project)=>
logger.log user_id:user_id, filePath:path, projectName:projectName, "handling new update from tpds"
if !project?
projectCreationHandler.createBlankProject user_id, projectName, (err, project)=>
# have a crack at setting the root doc after a while, on creation we won't have it yet, but should have
# been sent it it within 30 seconds
setTimeout (-> ProjectRootDocManager.setRootDocAutomatically project._id, sl_req_id ), @_rootDocTimeoutLength
cb err, project
else
cb err, project
getOrCreateProject (err, project)->
versioningApiHandler.takeSnapshot project._id, commitMessage, sl_req_id, ->
updateMerger.mergeUpdate project._id, path, updateRequest, sl_req_id, (err)->
callback(err)
deleteUpdate: (user_id, projectName, path, sl_req_id, callback)->
logger.log user_id:user_id, filePath:path, "handling delete update from tpds"
projectLocator.findUsersProjectByName user_id, projectName, (err, project)->
if !project?
logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project not found from tpds update, ignoring folder or project"
return callback()
if path == "/"
logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted"
return projectDeleter.markAsDeletedByExternalSource project._id, callback
else
versioningApiHandler.takeSnapshot project._id, commitMessage, sl_req_id, ->
updateMerger.deleteUpdate project._id, path, sl_req_id, (err)->
callback(err)
_rootDocTimeoutLength : 30 * 1000

View file

@ -0,0 +1,109 @@
settings = require('settings-sharelatex')
logger = require('logger-sharelatex')
slReqIdHelper = require('soa-req-id')
path = require('path')
Project = require('../../models/Project').Project
keys = require('../../infrastructure/Keys')
metrics = require("../../infrastructure/Metrics")
buildPath = (user_id, project_name, filePath)->
projectPath = path.join(project_name, "/", filePath)
projectPath = encodeURIComponent(projectPath)
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 =
 
addFile : (options, sl_req_id, callback = (err)->)->
metrics.inc("tpds.add-file")
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, rev:options.rev, "sending file to third party data store"
postOptions =
method : "post"
headers:
"sl_req_id":sl_req_id
sl_entity_rev:options.rev
sl_project_id:options.project_id
sl_all_user_ids:JSON.stringify(allUserIds)
uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}"
title:"addFile"
streamOrigin : settings.apis.filestore.url + path.join("/project/#{options.project_id}/file/","#{options.file_id}")
queue.enqueue options.project_id, "pipeStreamFrom", postOptions, ->
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, rev:options.rev, "sending file to third party data store queued up for processing"
callback()
addDoc : (options, sl_req_id, callback = (err)->)->
metrics.inc("tpds.add-doc")
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
return callback(err) if err?
logger.log project_id: options.project_id, user_id:user_id, path: options.path, rev:options.rev, uri:options.uri, project_name:options.project_name, docLines:options.docLines, sl_req_id:sl_req_id, "sending doc to third party data store"
postOptions =
method : "post"
headers:
"sl_req_id":sl_req_id,
sl_entity_rev:options.rev,
sl_project_id:options.project_id
sl_all_user_ids:JSON.stringify(allUserIds)
uri : "#{settings.apis.thirdPartyDataStore.url}#{buildPath(user_id, options.project_name, options.path)}"
title: "addDoc"
docLines: options.docLines
queue.enqueue options.project_id, "sendDoc", postOptions, callback
  
moveEntity : (options, sl_req_id, callback = (err)->)->
metrics.inc("tpds.move-entity")
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
if options.newProjectName?
startPath = path.join("/#{options.project_name}/")
endPath = path.join("/#{options.newProjectName}/")
else
startPath = mergeProjectNameAndPath(options.project_name, options.startPath)
endPath = mergeProjectNameAndPath(options.project_name, options.endPath)
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
logger.log project_id: options.project_id, user_id:user_id, startPath:startPath, endPath:endPath, uri:options.uri, sl_req_id:sl_req_id, "moving entity in third party data store"
moveOptions =
method : "put"
title:"moveEntity"
uri : "#{settings.apis.thirdPartyDataStore.url}/user/#{user_id}/entity"
headers:
"sl_req_id":sl_req_id,
sl_project_id:options.project_id,
sl_entity_rev:options.rev
sl_all_user_ids:JSON.stringify(allUserIds)
json :
user_id : user_id
endPath: endPath
startPath: startPath
queue.enqueue options.project_id, "standardHttpRequest", moveOptions, callback
deleteEntity : (options, sl_req_id, callback = (err)->)->
metrics.inc("tpds.delete-entity")
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, sl_req_id:sl_req_id, "deleting entity in third party data store"
deleteOptions =
method : "DELETE"
headers:
"sl_req_id":sl_req_id,
sl_project_id:options.project_id
sl_all_user_ids:JSON.stringify(allUserIds)
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
getProjectsUsersIds = (project_id, callback = (err, owner_id, allUserIds)->)->
Project.findById project_id, "_id owner_ref readOnly_refs collaberator_refs", (err, project)->
allUserIds = [].concat(project.collaberator_refs).concat(project.readOnly_refs).concat(project.owner_ref)
callback err, project.owner_ref, allUserIds
mergeProjectNameAndPath = (project_name, path)->
if(path.indexOf('/') == 0)
path = path.substring(1)
fullPath = "/#{project_name}/#{path}"
return fullPath

View file

@ -0,0 +1,116 @@
_ = require('underscore')
projectLocator = require('../Project/ProjectLocator')
editorController = require('../Editor/EditorController')
logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
slReqIdHelper = require('soa-req-id')
FileTypeManager = require('../Uploads/FileTypeManager')
GuidManager = require '../../managers/GuidManager'
fs = require('fs')
module.exports =
mergeUpdate: (project_id, path, updateRequest, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
self = @
logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "merging update from tpds"
projectLocator.findElementByPath project_id, path, (err, element)=>
logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "found element by path for merging update from tpds"
elementId = undefined
if element?
elementId = element._id
self.p.writeStreamToDisk project_id, elementId, updateRequest, (err, fsPath)->
FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
if shouldIgnore
return callback()
FileTypeManager.isBinary path, (err, isFile)->
if isFile
self.p.processFile project_id, elementId, fsPath, path, callback #TODO clean up the stream written to disk here
else
self.p.processDoc project_id, elementId, fsPath, path, sl_req_id, callback
deleteUpdate: (project_id, path, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
projectLocator.findElementByPath project_id, path, (err, element)->
type = 'file'
if err? || !element?
logger.log sl_req_id: sl_req_id, element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback()
if element.lines?
type = 'doc'
else if element.folders?
type = 'folder'
logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
editorController.deleteEntity project_id, element._id, type, sl_req_id, (err)->
logger.log sl_req_id: sl_req_id, project_id:project_id, path:path, "finished processing update to delete entity from tpds"
callback()
p:
processDoc: (project_id, doc_id, fsPath, path, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
readFileIntoTextArray fsPath, (err, docLines)->
if err?
logger.err project_id:project_id, doc_id:doc_id, fsPath:fsPath, "error reading file into text array for process doc update"
return callback(err)
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, sl_req_id:sl_req_id, "processing doc update from tpds"
if doc_id?
editorController.setDoc project_id, doc_id, docLines, sl_req_id, (err)->
callback()
else
setupNewEntity project_id, path, (err, folder, fileName)->
editorController.addDoc project_id, folder._id, fileName, docLines, sl_req_id, (err)->
callback()
processFile: (project_id, file_id, fsPath, path, sl_req_id, callback)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
finish = (err)->
logger.log sl_req_id: sl_req_id, project_id:project_id, file_id:file_id, path:path, "completed processing file update from tpds"
callback(err)
logger.log sl_req_id: sl_req_id, project_id:project_id, file_id:file_id, path:path, "processing file update from tpds"
setupNewEntity project_id, path, (err, folder, fileName) =>
if file_id?
editorController.replaceFile project_id, file_id, fsPath, finish
else
editorController.addFile project_id, folder._id, fileName, fsPath, finish
writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
if !file_id?
file_id = GuidManager.newGuid()
dumpPath = "#{Settings.path.dumpFolder}/#{project_id}_#{file_id}"
writeStream = fs.createWriteStream(dumpPath)
stream.pipe(writeStream)
stream.on 'error', (err)->
logger.err err:err, project_id:project_id, file_id:file_id, dumpPath:dumpPath,
"something went wrong with incoming tpds update stream"
writeStream.on 'error', (err)->
logger.err err:err, project_id:project_id, file_id:file_id, dumpPath:dumpPath,
"something went wrong with writing tpds update to disk"
stream.on 'end', ->
logger.log project_id:project_id, file_id:file_id, dumpPath:dumpPath, "incoming tpds update stream ended"
writeStream.on "finish", ->
logger.log project_id:project_id, file_id:file_id, dumpPath:dumpPath, "tpds update write stream finished"
callback null, dumpPath
if stream.emitBufferedData?
stream.emitBufferedData()
stream.resume()
readFileIntoTextArray = (path, callback)->
fs.readFile path, "utf8", (error, content = "") ->
if error?
logger.err path:path, "error reading file into text array"
return callback(err)
lines = content.split("\n")
callback error, lines
setupNewEntity = (project_id, path, callback)->
lastIndexOfSlash = path.lastIndexOf("/")
fileName = path[lastIndexOfSlash+1 .. -1]
folderPath = path[0 .. lastIndexOfSlash]
editorController.mkdirp project_id, folderPath, (err, newFolders, lastFolder)->
callback err, lastFolder, fileName

View file

@ -0,0 +1,23 @@
child = require "child_process"
logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics"
module.exports = ArchiveManager =
extractZipArchive: (source, destination, callback = (err) ->) ->
timer = new metrics.Timer("unzipDirectory")
logger.log source: source, destination: destination, "unzipping file"
unzip = child.spawn("unzip", [source, "-d", destination])
error = null
unzip.stderr.on "data", (chunk) ->
error ||= ""
error += chunk
unzip.on "exit", () ->
timer.done()
if error?
error = new Error(error)
logger.error err:error, source: source, destination: destination, "error unzipping file"
callback(error)

View file

@ -0,0 +1,65 @@
async = require "async"
fs = require "fs"
_ = require "underscore"
FileTypeManager = require "./FileTypeManager"
EditorController = require "../Editor/EditorController"
ProjectLocator = require "../Project/ProjectLocator"
module.exports = FileSystemImportManager =
addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
fs.readFile path, "utf8", (error, content = "") ->
return callback(error) if error?
content = content.replace(/\r/g, "")
lines = content.split("\n")
EditorController.addDoc project_id, folder_id, name, lines, callback
addFile: (project_id, folder_id, name, path, replace, callback = (error, file)-> )->
if replace
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
return callback(error) if error?
return callback(new Error("Couldn't find folder")) if !folder?
existingFile = null
for fileRef in folder.fileRefs
if fileRef.name == name
existingFile = fileRef
break
if existingFile?
EditorController.replaceFile project_id, existingFile._id, path, callback
else
EditorController.addFile project_id, folder_id, name, path, callback
else
EditorController.addFile project_id, folder_id, name, path, callback
addFolder: (project_id, folder_id, name, path, replace, callback = (error)-> ) ->
EditorController.addFolder project_id, folder_id, name, (error, new_folder) =>
return callback(error) if error?
@addFolderContents project_id, new_folder._id, path, replace, (error) ->
return callback(error) if error?
callback null, new_folder
addFolderContents: (project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
fs.readdir folderPath, (error, entries = []) =>
return callback(error) if error?
jobs = _.map entries, (entry) =>
(callback) =>
FileTypeManager.shouldIgnore entry, (error, ignore) =>
return callback(error) if error?
if !ignore
@addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
else
callback()
async.parallelLimit jobs, 5, callback
addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
FileTypeManager.isDirectory path, (error, isDirectory) =>
return callback(error) if error?
if isDirectory
@addFolder project_id, folder_id, name, path, replace, callback
else
FileTypeManager.isBinary name, (error, isBinary) =>
return callback(error) if error?
if isBinary
@addFile project_id, folder_id, name, path, replace, callback
else
@addDoc project_id, folder_id, name, path, replace, callback

View file

@ -0,0 +1,49 @@
fs = require "fs"
Path = require("path")
module.exports = FileTypeManager =
TEXT_EXTENSIONS : [
"tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md"
]
IGNORE_EXTENSIONS : [
"dvi", "aux", "log", "ps", "toc", "out", "pdfsync"
# Index and glossary files
"nlo", "ind", "glo", "gls", "glg"
# Bibtex
"bbl", "blg"
# Misc/bad
"doc", "docx", "gz"
]
IGNORE_FILENAMES : [
"__MACOSX"
]
isDirectory: (path, callback = (error, result) ->) ->
fs.stat path, (error, stats) ->
callback(error, stats.isDirectory())
isBinary: (path, callback = (error, result) ->) ->
parts = path.split(".")
extension = parts.slice(-1)[0]
if extension?
extension = extension.toLowerCase()
callback null, @TEXT_EXTENSIONS.indexOf(extension) == -1 or parts.length <= 1
shouldIgnore: (path, callback = (error, result) ->) ->
name = Path.basename(path)
extension = name.split(".").slice(-1)[0]
if extension?
extension = extension.toLowerCase()
ignore = false
if name[0] == "."
ignore = true
if @IGNORE_EXTENSIONS.indexOf(extension) != -1
ignore = true
if @IGNORE_FILENAMES.indexOf(name) != -1
ignore = true
callback null, ignore

View file

@ -0,0 +1,49 @@
logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics"
fs = require "fs"
Path = require "path"
FileSystemImportManager = require "./FileSystemImportManager"
ProjectUploadManager = require "./ProjectUploadManager"
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")
ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
fs.unlink path, ->
timer.done()
if error?
logger.error
err: error, file_path: path, file_name: name,
"error uploading project"
res.send success: false
else
logger.log
project: project._id, file_path: path, file_name: name,
"uploaded project"
res.send success: true, project_id: project._id
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
{name, path} = req.files.qqfile
project_id = req.params.Project_id
folder_id = req.query.folder_id
FileSystemImportManager.addEntity project_id, folder_id, name, path, true, (error, entity) ->
fs.unlink path, ->
timer.done()
if error?
logger.error
err: error, project_id: project_id, file_path: path,
file_name: name, folder_id: folder_id,
"error uploading file"
res.send success: false
else
logger.log
project_id: project_id, file_path: path, file_name: name, folder_id: folder_id
"uploaded file"
res.send success: true, entity_id: entity?._id

View file

@ -0,0 +1,28 @@
path = require "path"
rimraf = require "rimraf"
ArchiveManager = require "./ArchiveManager"
FileSystemImportManager = require "./FileSystemImportManager"
ProjectCreationHandler = require "../Project/ProjectCreationHandler"
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
module.exports = ProjectUploadHandler =
createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) ->
ProjectCreationHandler.createBlankProject owner_id, name, (error, project) =>
return callback(error) if error?
@insertZipArchiveIntoFolder project._id, project.rootFolder[0]._id, zipPath, (error) ->
return callback(error) if error?
ProjectRootDocManager.setRootDocAutomatically project._id, (error) ->
return callback(error) if error?
callback(error, project)
insertZipArchiveIntoFolder: (project_id, folder_id, path, callback = (error) ->) ->
destination = @_getDestinationDirectory path
ArchiveManager.extractZipArchive path, destination, (error) ->
return callback(error) if error?
FileSystemImportManager.addFolderContents project_id, folder_id, destination, false, (error) ->
return callback(error) if error?
rimraf(destination, callback)
_getDestinationDirectory: (source) ->
return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}")

View file

@ -0,0 +1,13 @@
SecurityManager = require('../../managers/SecurityManager')
AuthenticationController = require('../Authentication/AuthenticationController')
ProjectUploadController = require "./ProjectUploadController"
module.exports =
apply: (app) ->
app.post '/project/new/upload',
AuthenticationController.requireLogin(),
ProjectUploadController.uploadProject
app.post '/Project/:Project_id/upload',
SecurityManager.requestCanModifyProject,
ProjectUploadController.uploadFile

View file

@ -0,0 +1,35 @@
UserGetter = require "./UserGetter"
logger = require("logger-sharelatex")
module.exports = UserController =
getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) ->
# 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
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?
UserController.sendFormattedPersonalInfo(req.user, res, next)
getPersonalInfo: (req, res, next = (error) ->) ->
UserGetter.getUser req.params.user_id, { _id: true, first_name: true, last_name: true, email: true }, (error, user) ->
logger.log user: req.params.user_id, "reciving request for getting users personal info"
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) ->
return next(error) if error?
res.send JSON.stringify(info)
_formatPersonalInfo: (user, callback = (error, info) ->) ->
callback null, {
id: user._id.toString()
first_name: user.first_name
last_name: user.last_name
email: user.email
signUpDate: user.signUpDate
}

View file

@ -0,0 +1,19 @@
User = require("../../models/User").User
UserLocator = require("./UserLocator")
module.exports =
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
self = @
UserLocator.findByEmail email, (err, user)->
if user?
callback(err, user)
else
self.createNewUser email:email, holdingAccount:true, callback
createNewUser: (opts, callback)->
user = new User()
user.email = opts.email
user.holdingAccount = opts.holdingAccount
user.save (err)->
callback(err, user)

View file

@ -0,0 +1,22 @@
User = require("../../models/User").User
NewsletterManager = require "../../managers/NewsletterManager"
ProjectDeleter = require("../Project/ProjectDeleter")
logger = require("logger-sharelatex")
module.exports =
deleteUser: (user_id, callback = ()->)->
if !user_id?
logger.err "user_id is null when trying to delete user"
return callback("no user_id")
User.findById user_id, (err, user)->
logger.log user:user, "deleting user"
if err?
return callback(err)
NewsletterManager.unsubscribe user, (err)->
if err?
return callback(err)
ProjectDeleter.deleteUsersProjects user._id, (err)->
if err?
return callback(err)
user.remove callback

View file

@ -0,0 +1,15 @@
mongojs = require("../../infrastructure/mongojs")
db = mongojs.db
ObjectId = mongojs.ObjectId
module.exports = UserGetter =
getUser: (query, projection, callback = (error, user) ->) ->
if arguments.length == 2
callback = projection
projection = {}
if typeof query == "string"
query = _id: ObjectId(query)
else if query instanceof ObjectId
query = _id: query
db.users.findOne query, projection, callback

View file

@ -0,0 +1,13 @@
mongojs = require("../../infrastructure/mongojs")
db = mongojs.db
ObjectId = mongojs.ObjectId
module.exports =
findByEmail: (email, callback)->
email = email.trim()
db.users.findOne email:email, (err, user)->
callback(err, user)
findById: (_id, callback)->
db.users.findOne _id:ObjectId(_id+""), callback

View file

@ -0,0 +1,30 @@
sanitize = require('validator').sanitize
module.exports =
validateEmail : (email) ->
re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(email)
hasZeroLengths : (props) ->
hasZeroLength = false
props.forEach (prop) ->
if prop.length == 0
hasZeroLength = true
return hasZeroLength
validateRegisterRequest : (req, callback)->
email = sanitize(req.body.email).xss().trim().toLowerCase()
password = req.body.password
username = email.match(/^[^@]*/)
if username?
first_name = username[0]
else
first_name = ""
last_name = ""
if @hasZeroLengths([password, email])
callback('please fill in all the fields', null)
else if !@validateEmail(email)
callback('not valid email', null)
else
callback(null, {first_name:first_name, last_name:last_name, email:email, password:password})

View file

@ -0,0 +1,12 @@
mongojs = require("../../infrastructure/mongojs")
db = mongojs.db
ObjectId = mongojs.ObjectId
module.exports = UserUpdater =
updateUser: (query, update, callback = (error) ->) ->
if typeof query == "string"
query = _id: ObjectId(query)
else if query instanceof ObjectId
query = _id: query
db.users.update query, update, callback

View file

@ -0,0 +1,50 @@
Keys = require("./RedisKeys")
Settings = require "settings-sharelatex"
redis = require('redis')
rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host)
rclient.auth(Settings.redis.web.password)
VersioningApiHandler = require('../../Features/Versioning/VersioningApiHandler')
async = require('async')
metrics = require('../../infrastructure/Metrics')
logger = require('logger-sharelatex')
module.exports = AutomaticSnapshotManager =
markProjectAsUpdated: (project_id, callback = (error) ->) ->
rclient.set Keys.buildLastUpdatedKey(project_id), Date.now(), (error) ->
return callback(error) if error?
rclient.sadd Keys.projectsToSnapshotKey, project_id, (error) ->
return callback(error) if error?
callback()
unmarkProjectAsUpdated: (project_id, callback = (err)->)->
rclient.del Keys.buildLastUpdatedKey(project_id), Date.now(), (error) ->
return callback(error) if error?
rclient.srem Keys.projectsToSnapshotKey, project_id, (error) ->
return callback(error) if error?
callback()
takeAutomaticSnapshots: (callback = (error) ->) ->
rclient.smembers Keys.projectsToSnapshotKey, (error, project_ids) =>
logger.log project_ids:project_ids, "taking automatic snapshots"
metrics.gauge "versioning.projectsToSnapshot", project_ids.length
return callback(error) if error?
methods = []
for project_id in project_ids
do (project_id) =>
methods.push((callback) => @takeSnapshotIfRequired(project_id, callback))
async.series methods, callback
takeSnapshotIfRequired: (project_id, callback = (error) ->) ->
rclient.get Keys.buildLastUpdatedKey(project_id), (error, lastUpdated) ->
return callback(error) if error?
if lastUpdated? and lastUpdated < Date.now() - Settings.automaticSnapshots.waitTimeAfterLastEdit
VersioningApiHandler.takeSnapshot(project_id, "Automatic snapshot", callback)
else
rclient.get Keys.buildLastSnapshotKey(project_id), (error, lastSnapshot) ->
return callback(error) if error?
if !lastSnapshot? or lastSnapshot < Date.now() - Settings.automaticSnapshots.maxTimeBetweenSnapshots
VersioningApiHandler.takeSnapshot(project_id, "Automatic snapshot", callback)
else
callback()

View file

@ -0,0 +1,5 @@
module.exports =
buildLastUpdatedKey: (project_id) -> "project_last_updated:#{project_id}"
buildLastSnapshotKey: (project_id) -> "project_last_snapshot:#{project_id}"
projectsToSnapshotKey: "projects_to_snapshot"
usersToPollTpdsForUpdates: "users_with_active_projects"

View file

@ -0,0 +1,31 @@
versioningApiHandler = require './VersioningApiHandler'
metrics = require('../../infrastructure/Metrics')
module.exports =
enableVersioning: (project_id, callback)->
metrics.inc "versioning.enableVersioning"
versioningApiHandler.enableVersioning project_id, callback
listVersions : (req, res) ->
metrics.inc "versioning.listVersions"
versioningApiHandler.proxyToVersioningApi(req, res)
getVersion : (req, res) ->
metrics.inc "versioning.getVersion"
versioningApiHandler.proxyToVersioningApi(req, res)
getVersionFile : (req, res) ->
metrics.inc "versioning.getVersionFile"
versioningApiHandler.proxyToVersioningApi(req, res)
takeSnapshot: (req, res, next) ->
metrics.inc "versioning.takeSnapshot"
if req.body? and req.body.message? and req.body.message.length > 0
message = req.body.message
else
message = "Manual snapshot"
versioningApiHandler.takeSnapshot req.params.Project_id, message, (error) ->
if error?
next(error)
else
res.send(200, "{}")

View file

@ -0,0 +1,75 @@
settings = require('settings-sharelatex')
logger = require('logger-sharelatex')
Project = require('../../models/Project').Project
request = require('request')
DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
redis = require('redis')
rclient = redis.createClient(settings.redis.web.port, settings.redis.web.host)
rclient.auth(settings.redis.web.password)
Keys = require("./RedisKeys")
ProjectEntityHandler = require('../../Features/Project/ProjectEntityHandler')
metrics = require('../../infrastructure/Metrics')
keys = require('../../infrastructure/Keys')
queue = require('fairy').connect(settings.redis.fairy).queue(keys.queue.web_to_tpds_http_requests)
slReqIdHelper = require('soa-req-id')
headers =
Authorization : "Basic " + new Buffer("#{settings.apis.versioning.username}:#{settings.apis.versioning.password}").toString("base64")
module.exports =
enableVersioning: (project_or_id, callback = (err)->)->
Project.getProject project_or_id, 'existsInVersioningApi', (error, project)=>
return callback error if error?
return callback new Error("project_id:#{project_id} does not exist") if !project?
project_id = project._id
if project.existsInVersioningApi
logger.log project_id: project_id, "versioning already enabled"
return callback()
logger.log project_id: project_id, "enabling versioning in versioning API"
@createProject project_id, (error) ->
return callback error if error?
logger.log project_id: project_id, "enabling versioning in Mongo"
project.existsInVersioningApi = true
update = existsInVersioningApi : true
conditions = _id:project_id
Project.update conditions, update, {}, ->
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (err) ->
callback(err)
proxyToVersioningApi : (req, res) ->
metrics.inc "versioning.proxy"
options =
url : settings.apis.versioning.url + req.url
headers : headers
logger.log url: req.url, "proxying to versioning api"
getReq = request.get(options)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "versioning API error"
res.send 500
createProject : (project_id, callback) ->
url = "#{settings.apis.versioning.url}/project/#{project_id}"
options = {method:"post", url:url, headers:headers, title:"createVersioningProject"}
queue.enqueue project_id, "standardHttpRequest", options, callback
takeSnapshot: (project_id, message, sl_req_id, callback = (error) ->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
logger.log project_id: project_id, sl_req_id: sl_req_id, "taking snapshot of project"
# This isn't critical so we can do it async
rclient.set Keys.buildLastSnapshotKey(project_id), Date.now(), () ->
rclient.srem Keys.projectsToSnapshotKey, project_id, () ->
DocumentUpdaterHandler.flushProjectToMongo project_id, sl_req_id, (err) ->
return callback(err) if err?
url = "#{settings.apis.versioning.url}/project/#{project_id}/version"
json = version:{message:message}
options = {method:"post", json:json, url:url, headers:headers, title:"takeVersioningSnapshot"}
queue.enqueue project_id, "standardHttpRequest", options, ->
logger.log options:options, project_id, "take snapshot enqueued"
callback()

View file

@ -0,0 +1,128 @@
logger = require('logger-sharelatex')
_ = require('underscore')
User = require('../models/User').User
Quote = require('../models/Quote').Quote
Project = require('../models/Project').Project
DocumentUpdaterHandler = require('../Features/DocumentUpdater/DocumentUpdaterHandler')
Settings = require('settings-sharelatex')
util = require('util')
redis = require('redis')
rclient = redis.createClient(Settings.redis.web.port, Settings.redis.web.host)
rclient.auth(Settings.redis.web.password)
RecurlyWrapper = require('../Features/Subscription/RecurlyWrapper')
SubscriptionHandler = require('../Features/Subscription/SubscriptionHandler')
projectEntityHandler = require('../Features/Project/ProjectEntityHandler')
TpdsPollingBackgroundTasks = require("../Features/ThirdPartyDataStore/TpdsPollingBackgroundTasks")
EditorRealTimeController = require("../Features/Editor/EditorRealTimeController")
module.exports = AdminController =
index : (req, res)=>
http = require('http')
openSockets = {}
for url, agents of require('http').globalAgent.sockets
openSockets["http://#{url}"] = (agent._httpMessage.path for agent in agents)
for url, agents of require('https').globalAgent.sockets
openSockets["https://#{url}"] = (agent._httpMessage.path for agent in agents)
memory = process.memoryUsage()
io = require("../infrastructure/Server").io
allUsers = io.sockets.clients()
users = []
allUsers.forEach (user)->
u = {}
user.get "email", (err, email)->
u.email = email
user.get "first_name", (err, first_name)->
u.first_name = first_name
user.get "last_name", (err, last_name)->
u.last_name = last_name
user.get "project_id", (err, project_id)->
u.project_id = project_id
user.get "user_id", (err, user_id)->
u.user_id = user_id
user.get "signup_date", (err, signup_date)->
u.signup_date = signup_date
user.get "login_count", (err, login_count)->
u.login_count = login_count
user.get "connected_time", (err, connected_time)->
now = new Date()
connected_mins = (((now - new Date(connected_time))/1000)/60).toFixed(2)
u.connected_mins = connected_mins
users.push u
d = new Date()
today = d.getDate()+":"+(d.getMonth()+1)+":"+d.getFullYear()+":"
yesterday = (d.getDate()-1)+":"+(d.getMonth()+1)+":"+d.getFullYear()+":"
multi = rclient.multi()
multi.get today+"docsets"
multi.get yesterday+"docsets"
multi.exec (err, replys)->
redisstats =
today:
docsets: replys[0]
compiles: replys[1]
yesterday:
docsets: replys[2]
compiles: replys[3]
DocumentUpdaterHandler.getNumberOfDocsInMemory (err, numberOfInMemoryDocs)=>
User.count (err, totalUsers)->
Project.count (err, totalProjects)->
res.render 'admin',
title: 'System Admin'
currentConnectedUsers:allUsers.length
users: users
numberOfAceDocs : numberOfInMemoryDocs
totalUsers: totalUsers
totalProjects: totalProjects
openSockets: openSockets
redisstats: redisstats
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)
closeEditor : (req, res)->
logger.warn "closing editor"
Settings.editorIsOpen = req.body.isOpen
res.send(200)
writeAllToMongo : (req, res)->
logger.log "writing all docs to mongo"
Settings.mongo.writeAll = true
DocumentUpdaterHandler.flushAllDocsToMongo ()->
logger.log "all docs have been saved to mongo"
res.send()
addQuote : (req, res)->
quote = new Quote
author: req.body.author
quote: req.body.quote
quote.save (err)->
res.send 200
syncUserToSubscription: (req, res)->
{user_id, subscription_id} = req.body
RecurlyWrapper.getSubscription subscription_id, {}, (err, subscription)->
User.findById user_id, (err, user)->
SubscriptionHandler.syncSubscriptionToUser subscription, user._id, (err)->
logger.log user_id:user_id, subscription_id:subscription_id, "linked account to subscription"
res.send()
flushProjectToTpds: (req, res)->
projectEntityHandler.flushProjectToThirdPartyDataStore req.body.project_id, (err)->
res.send 200
pollUsersWithDropbox: (req, res)->
TpdsPollingBackgroundTasks.pollUsersWithDropbox ->
res.send 200
updateProjectCompiler: (req, res, next = (error) ->)->
Project.findOne _id: req.body.project_id, (error, project) ->
return next(error) if error?
project.useClsi2 = (req.body.new == "new")
logger.log project_id: req.body.project_id, useClsi2: project.useClsi2, "updating project compiler"
project.save (error) ->
return next(error) if error?
res.send(200)

View file

@ -0,0 +1,53 @@
logger = require('logger-sharelatex')
_ = require('underscore')
User = require('./UserController')
Quotes = require('../models/Quote').Quote
module.exports =
index : (req,res)->
if req.session.user
if req.query.scribtex_path?
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
else
res.redirect '/project'
else
res.render 'homepage/home',
title: 'ShareLaTeX.com'
comments : (req, res)->
res.render 'homepage/comments.jade',
title: 'User Comments'
resources : (req, res)->
res.render 'resources.jade',
title: 'LaTeX Resources'
tos : (req, res) ->
res.render 'about/tos',
title: "Terms of Service"
privacy : (req, res) ->
res.render 'about/privacy',
title: "Privacy Policy"
about : (req, res) ->
res.render 'about/about',
title: "About us"
notFound: (req, res)->
res.statusCode = 404
res.render 'general/404',
title: "Page Not Found"
security : (req, res) ->
res.render 'about/security',
title: "Security"
attribution: (req, res) ->
res.render 'about/attribution',
title: "Attribution"
planned_maintenance: (req, res) ->
res.render 'about/planned_maintenance',
title: "Planned Maintenance"

View file

@ -0,0 +1,12 @@
module.exports=
themes : (req, res)=>
res.render "info/themes",
title: 'Themes'
dropbox: (req, res)->
res.render "info/dropbox",
title: 'Dropbox with LaTeX'
advisor: (req, res)->
res.render "info/advisor",
title: 'Advisor Program'

View file

@ -0,0 +1,218 @@
User = require('../models/User').User
Project = require('../models/Project').Project
sanitize = require('validator').sanitize
path = require "path"
logger = require('logger-sharelatex')
_ = require('underscore')
fs = require('fs')
ProjectHandler = require '../handlers/ProjectHandler'
SecurityManager = require '../managers/SecurityManager'
GuidManager = require '../managers/GuidManager'
Settings = require('settings-sharelatex')
projectCreationHandler = require '../Features/Project/ProjectCreationHandler'
projectLocator = require '../Features/Project/ProjectLocator'
projectDuplicator = require('../Features/Project/ProjectDuplicator')
ProjectZipStreamManager = require '../Features/Downloads/ProjectZipStreamManager'
metrics = require('../infrastructure/Metrics')
TagsHandler = require('../Features/Tags/TagsHandler')
SubscriptionLocator = require("../Features/Subscription/SubscriptionLocator")
SubscriptionFormatters = require("../Features/Subscription/SubscriptionFormatters")
FileStoreHandler = require("../Features/FileStore/FileStoreHandler")
module.exports = class ProjectController
constructor: (@collaberationManager)->
ProjectHandler = new ProjectHandler()
list: (req, res, next)->
timer = new metrics.Timer("project-list")
user_id = req.session.user._id
startTime = new Date()
User.findById user_id, (error, user) ->
logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - User.findById"
startTime = new Date()
# TODO: Remove this one month after the ability to start free trials was removed
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Subscription.getUsersSubscription"
startTime = new Date()
return next(error) if error?
# TODO: Remove this one month after the ability to start free trials was removed
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
freeTrial =
expired: !!subscription.freeTrial.downgraded
expiresAt: SubscriptionFormatters.formatDate(subscription.freeTrial.expiresAt)
TagsHandler.getAllTags user_id, (err, tags, tagsGroupedByProject)->
logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - TagsHandler.getAllTags"
startTime = new Date()
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel', (projects, collabertions, readOnlyProjects)->
logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Project.findAllUsersProjects"
startTime = new Date()
for project in projects
project.accessLevel = "owner"
for project in collabertions
project.accessLevel = "readWrite"
for project in readOnlyProjects
project.accessLevel = "readOnly"
projects = projects.concat(collabertions).concat(readOnlyProjects)
projects = projects.map (project)->
project.tags = tagsGroupedByProject[project._id] || []
return project
tags = _.sortBy tags, (tag)->
-tag.project_ids.length
logger.log projects:projects, collabertions:collabertions, readOnlyProjects:readOnlyProjects, user_id:user_id, "rendering project list"
sortedProjects = _.sortBy projects, (project)->
return - project.lastUpdated
res.render 'project/list',
title:'Your Projects'
priority_title: true
projects: sortedProjects
freeTrial: freeTrial
tags:tags
projectTabActive: true
logger.log user_id: user_id, duration: (new Date() - startTime), "project list timer - Finished"
timer.done()
apiNewProject: (req, res)->
user = req.session.user
projectName = sanitize(req.body.projectName).xss()
template = sanitize(req.body.template).xss()
logger.log user: user, type: template, name: projectName, "creating project"
if template == 'example'
projectCreationHandler.createExampleProject user._id, projectName, (err, project)->
if err?
logger.error err: err, project: project, user: user, name: projectName, type: "example", "error creating project"
res.send 500
else
logger.log project: project, user: user, name: projectName, type: "example", "created project"
res.send {project_id:project._id}
else
projectCreationHandler.createBasicProject user._id, projectName, (err, project)->
if err?
logger.error err: err, project: project, user: user, name: projectName, type: "basic", "error creating project"
res.send 500
else
logger.log project: project, user: user, name: projectName, type: "basic", "created project"
res.send {project_id:project._id}
loadEditor: (req, res)->
timer = new metrics.Timer("load-editor")
if !Settings.editorIsOpen
res.render("general/closed", {title:"updating site"})
else
if req.session.user?
user_id = req.session.user._id
else
user_id = 'openUser'
project_id = req.params.Project_id
Project.findPopulatedById project_id, (err, project)->
User.findById user_id, (err, user)->
if user_id == 'openUser'
anonymous = true
user =
id : user_id
ace:
mode:'none'
theme:'textmate'
fontSize: '12'
autoComplete: true
spellCheckLanguage: ""
pdfViewer: ""
subscription:
freeTrial:
allowed: true
featureSwitches:
dropbox: false
longPolling: false
else
anonymous = false
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
SecurityManager.userCanAccessProject user, project, (canAccess, privlageLevel)->
allowedFreeTrial = true
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
allowedFreeTrial = !!subscription.freeTrial.allowed
if canAccess
timer.done()
res.render 'project/editor',
title: project.name
priority_title: true
bodyClasses: ["editor"]
project : project
owner : project.owner_ref
userObject : JSON.stringify({
id : user.id
email : user.email
first_name : user.first_name
last_name : user.last_name
referal_id : user.referal_id
subscription :
freeTrial: {allowed: allowedFreeTrial}
})
userSettingsObject: JSON.stringify({
mode : user.ace.mode
theme : user.ace.theme
project_id : project._id
fontSize : user.ace.fontSize
autoComplete: user.ace.autoComplete
spellCheckLanguage: user.ace.spellCheckLanguage
pdfViewer : user.ace.pdfViewer
docPositions: {}
longPolling: user.featureSwitches.longPolling
})
sharelatexObject : JSON.stringify({
siteUrl: Settings.siteUrl,
jsPath: res.locals.jsPath
})
privlageLevel: privlageLevel
userCanSeeDropbox: user.featureSwitches.dropbox and project.owner_ref._id+"" == user._id+""
loadPdfjs: (user.ace.pdfViewer == "pdfjs")
chatUrl: Settings.apis.chat.url
anonymous: anonymous
languages: Settings.languages,
startBufferingRequest: (req, res, next) ->
req.bufferedChunks = []
req.endEmitted = false
bufferChunk = (chunk) -> req.bufferedChunks.push(chunk)
req.on "data", bufferChunk
endCallback = () -> req.endEmitted = true
req.on "end", endCallback
req.emitBufferedData = () ->
logger.log chunks: @bufferedChunks.length, emittedEnd: @endEmitted, "emitting buffer chunks"
@removeListener "data", bufferChunk
while @bufferedChunks.length > 0
@emit "data", @bufferedChunks.shift()
@removeListener "end", endCallback
@emit "end" if @endEmitted
next()
downloadImageFile : (req, res)->
project_id = req.params.Project_id
file_id = req.params.File_id
queryString = req.query
logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download"
res.setHeader("Content-Disposition", "attachment")
FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)->
stream.pipe res
cloneProject: (req, res)->
metrics.inc "cloned-project"
project_id = req.params.Project_id
projectName = req.body.projectName
logger.log project_id:project_id, projectName:projectName, "cloning project"
if !req.session.user?
return res.send redir:"/register"
projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)->
if err?
logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project"
return next(err)
res.send(project_id:project._id)
deleteProject: (req, res)->
project_id = req.params.Project_id
logger.log project_id:project_id, "deleting project"
ProjectHandler.deleteProject project_id, (err)->
if err?
res.send 500
else
res.send 200

View file

@ -0,0 +1,237 @@
User = require('../models/User').User
sanitize = require('validator').sanitize
fs = require('fs')
_ = require('underscore')
emailer = require('../managers/EmailManager')
logger = require('logger-sharelatex')
Security = require('../managers/SecurityManager')
Settings = require('settings-sharelatex')
newsLetterManager = require('../managers/NewsletterManager')
dropboxHandler = require('../Features/Dropbox/DropboxHandler')
userRegistrationHandler = require('../Features/User/UserRegistrationHandler')
metrics = require('../infrastructure/Metrics')
AnalyticsManager = require('../Features/Analytics/AnalyticsManager')
ReferalAllocator = require('../Features/Referal/ReferalAllocator')
AuthenticationManager = require("../Features/Authentication/AuthenticationManager")
AuthenticationController = require("../Features/Authentication/AuthenticationController")
SubscriptionLocator = require("../Features/Subscription/SubscriptionLocator")
UserDeleter = require("../Features/User/UserDeleter")
Url = require("url")
module.exports =
registerForm : (req, res)->
sharedProjectData =
project_name:req.query.project_name
user_first_name:req.query.user_first_name
newTemplateData = {}
if req.session.templateData?
newTemplateData.templateName = req.session.templateData.templateName
res.render 'user/register',
title: 'Register'
redir: req.query.redir
sharedProjectData: sharedProjectData
newTemplateData: newTemplateData
new_email:req.query.new_email || ""
loginForm : (req, res)->
res.render 'user/login',
title: 'Login',
redir: req.query.redir
apiRegister : (req, res, next = (error) ->)->
logger.log email: req.body.email, "attempted register"
redir = Url.parse(req.body.redir or "/project").path
userRegistrationHandler.validateRegisterRequest req, (err, data)->
if err?
logger.log validation_error: err, "user validation error"
metrics.inc "user.register.validation-error"
res.send message:
text:err
type:'error'
else
User.findOne {email:data.email}, (err, foundUser)->
if foundUser? && foundUser.holdingAccount == false
AuthenticationController.login req, res
logger.log email: data.email, "email already registered"
metrics.inc "user.register.already-registered"
return AuthenticationController.login req, res
else if foundUser? && foundUser.holdingAccount == true #someone put them in as a collaberator
user = foundUser
user.holdingAccount == false
else
user = new User email: data.email
d = new Date()
user.first_name = data.first_name
user.last_name = data.last_name
user.signUpDate = new Date()
metrics.inc "user.register.success"
user.save (err)->
req.session.user = user
req.session.justRegistered = true
logger.log user: user, "registered"
AuthenticationManager.setUserPassword user._id, data.password, (error) ->
return next(error) if error?
res.send
redir:redir
id:user._id.toString()
first_name: user.first_name
last_name: user.last_name
email: user.email
created: Date.now()
#things that can be fired and forgot.
newsLetterManager.subscribe user
ReferalAllocator.allocate req.session.referal_id, user._id, req.session.referal_source, req.session.referal_medium
requestPasswordReset : (req, res)->
res.render 'user/passwordReset',
title: 'Password Reset',
doRequestPasswordReset : (req, res, next = (error) ->)->
email = sanitize(req.body.email).xss()
email = sanitize(email).trim()
email = email.toLowerCase()
logger.log email: email, "password reset requested"
User.findOne {'email':email}, (err, user)->
if(user?)
randomPassword = generateRandomString 12
AuthenticationManager.setUserPassword user._id, randomPassword, (error) ->
return next(error) if error?
emailOptions =
receiver : user.email
subject : "Password Reset - ShareLatex.com"
heading : "Password Reset"
message : " Your password has been reset, the new password is <p> #{randomPassword}
<p> please login <a href=#{Settings.siteUrl}/user/settings>click here</a>
"
emailer.sendEmail emailOptions
metrics.inc "user.password-reset"
res.send message:
text:'An email with your new password has been sent to you'
type:'success'
else
res.send message:
text:'This email address has not been registered with us'
type:'failure'
logger.info email: email, "no user found with email"
logout : (req, res)->
metrics.inc "user.logout"
if req.session? && req.session.user?
logger.log user: req.session.user, "logging out"
req.session.destroy (err)->
if err
logger.err err: err, 'error destorying session'
res.redirect '/login'
settings : (req, res)->
logger.log user: req.session.user, "loading settings page"
User.findById req.session.user._id, (err, user)->
dropboxHandler.getUserRegistrationStatus user._id, (err, status)->
userIsRegisteredWithDropbox = !err? and status.registered
res.render 'user/settings',
title:'Your settings',
userCanSeeDropbox: user.featureSwitches.dropbox
userHasDropboxFeature: user.features.dropbox
userIsRegisteredWithDropbox: userIsRegisteredWithDropbox
user: user,
themes: THEME_LIST,
editors: ['default','vim','emacs'],
fontSizes: ['10','11','12','13','14','16','20','24']
languages: Settings.languages,
accountSettingsTabActive: true
unsubscribe: (req, res)->
User.findById req.session.user._id, (err, user)->
newsLetterManager.unsubscribe user, ->
res.send()
apiUpdate : (req, res)->
logger.log user: req.session.user, "updating account settings"
metrics.inc "user.settings-update"
User.findById req.session.user._id, (err, user)->
if(user)
user.first_name = sanitize(req.body.first_name).xss().trim()
user.last_name = sanitize(req.body.last_name).xss().trim()
user.ace.mode = sanitize(req.body.mode).xss().trim()
user.ace.theme = sanitize(req.body.theme).xss().trim()
user.ace.fontSize = sanitize(req.body.fontSize).xss().trim()
user.ace.autoComplete = req.body.autoComplete == "true"
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
user.ace.pdfViewer = req.body.pdfViewer
user.save()
res.send {}
changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change"
oldPass = req.body.currentPassword
AuthenticationManager.authenticate _id: req.session.user._id, oldPass, (err, user)->
if(user)
logger.log user: req.session.user, "changing password"
newPassword1 = req.body.newPassword1
newPassword2 = req.body.newPassword2
if newPassword1 != newPassword2
logger.log user: user, "passwords do not match"
res.send
message:
type:'error'
text:'Your passwords do not match'
else
logger.log user: user, "password changed"
AuthenticationManager.setUserPassword user._id, newPassword1, (error) ->
return next(error) if error?
res.send
message:
type:'success'
text:'Your password has been changed'
else
logger.log user: user, "current password wrong"
res.send
message:
type:'error'
text:'Your old password is wrong'
redirectUserToDropboxAuth: (req, res)->
user_id = req.session.user._id
dropboxHandler.getDropboxRegisterUrl user_id, (err, url)->
logger.log url:url, "redirecting user for dropbox auth"
res.redirect url
completeDropboxRegistration: (req, res)->
user_id = req.session.user._id
dropboxHandler.completeRegistration user_id, (err, success)->
res.redirect('/user/settings#dropboxSettings')
unlinkDropbox: (req, res)->
user_id = req.session.user._id
dropboxHandler.unlinkAccount user_id, (err, success)->
res.redirect('/user/settings#dropboxSettings')
deleteUser: (req, res)->
user_id = req.session.user._id
UserDeleter.deleteUser user_id, (err)->
if !err?
req.session.destroy()
res.send(200)
generateRandomString = (len)->
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"
randomString = ''
count = 0
while count++ < len
rnum = Math.floor(Math.random() * chars.length)
randomString += chars.substring(rnum,rnum+1)
return randomString
THEME_LIST = []
do generateThemeList = () ->
files = fs.readdirSync __dirname + '/../../../public/js/ace/theme'
for file in files
if file.slice(-2) == "js"
cleanName = file.slice(0,-3)
THEME_LIST.push name: cleanName

View file

@ -0,0 +1,10 @@
NotFoundError = (message) ->
error = new Error(message)
error.name = "NotFoundError"
error.__proto__ = NotFoundError.prototype
return error
NotFoundError.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError

View file

@ -0,0 +1,164 @@
Project = require('../models/Project').Project
Folder = require('../models/Folder').Folder
Doc = require('../models/Doc').Doc
File = require('../models/File').File
User = require('../models/User').User
logger = require('logger-sharelatex')
_ = require('underscore')
Settings = require('settings-sharelatex')
emailer = require('../managers/EmailManager')
tpdsUpdateSender = require '../Features/ThirdPartyDataStore/TpdsUpdateSender'
projectCreationHandler = require '../Features/Project/ProjectCreationHandler'
projectEntityHandler = require '../Features/Project/ProjectEntityHandler'
ProjectEditorHandler = require '../Features/Project/ProjectEditorHandler'
FileStoreHandler = require "../Features/FileStore/FileStoreHandler"
projectLocator = require '../Features/Project/ProjectLocator'
mimelib = require("mimelib")
async = require('async')
tagsHandler = require('../Features/Tags/TagsHandler')
module.exports = class ProjectHandler
getProject: (project_id, callback)->
logger.log project_id: project_id, "getting project"
Project.findById project_id, (err, project)->
callback err, ProjectEditorHandler.buildProjectModelView(project, includeUsers: false)
confirmFolder = (project_id, folder_id, callback)->
logger.log folder: folder_id, project_id: project_id, "confirming existence of folder"
if folder_id+'' == 'undefined'
Project.findById project_id, (err, project)->
callback(project.rootFolder[0]._id)
else if folder_id != null
callback folder_id
else
Project.findById project_id, (err, project)->
callback(project.rootFolder[0]._id)
renameEntity: (project_id, entity_id, entityType, newName, callback)->
logger.log(entity_id: entity_id, project_id: project_id, ('renaming '+entityType))
if !entityType?
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=>
if err?
return callback err
conditons = {_id:project_id}
update = "$set":{}
namePath = path.mongo+".name"
update["$set"][namePath] = newName
endPath = path.fileSystem.replace(entity.name, newName)
tpdsUpdateSender.moveEntity({project_id:project_id, startPath:path.fileSystem, endPath:endPath, project_name:project.name, rev:entity.rev})
Project.update conditons, update, {}, (err)->
if callback?
callback err
renameProject: (project_id, window_id, newName, callback)->
logger.log project_id: project_id, "renaming project"
conditons = {_id:project_id}
Project.findOne conditons, "name", (err, project)->
oldProjectName = project.name
Project.update conditons, {name: newName}, {},(err, project)=>
tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}
if callback?
callback err
deleteProject: (project_id, callback = (error) ->)->
logger.log project_id:project_id, "deleting project"
Project.findById project_id, (err, project)=>
if project?
require('../Features/DocumentUpdater/DocumentUpdaterHandler').flushProjectToMongoAndDelete project_id, (error) ->
return callback(error) if error?
Project.applyToAllFilesRecursivly project.rootFolder[0], (file)=>
FileStoreHandler.deleteFile project_id, file._id, ->
Project.remove {_id:project_id}, (err)->
if callback?
callback(err)
require('../Features/Versioning/AutomaticSnapshotManager').unmarkProjectAsUpdated project_id, ->
tagsHandler.removeProjectFromAllTags project.owner_ref, project_id,->
project.collaberator_refs.forEach (collaberator_ref)->
tagsHandler.removeProjectFromAllTags collaberator_ref, project_id, ->
project.readOnly_refs.forEach (readOnly_ref)->
tagsHandler.removeProjectFromAllTags readOnly_ref, project_id,->
else
if callback?
callback(err)
setPublicAccessLevel : (project_id, newAccessLevel, callback)->
logger.log project_id: project_id, level: newAccessLevel, "set public access level"
if project_id? && newAccessLevel?
if _.include ['readOnly', 'readAndWrite', 'private'], newAccessLevel
Project.update {_id:project_id},{publicAccesLevel:newAccessLevel},{}, (err)->
if callback?
callback()
addUserToProject: (project_id, email, privlages, callback)->
if email != ''
doAdd = (user)=>
Project.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
receiver : email
replyTo : project.owner_ref.email
subject : "#{project.owner_ref.first_name} #{project.owner_ref.last_name} wants to share '#{project.name}' with you"
heading : "#{project.name} #{project.owner_ref.last_name} wants to share '#{project.name}' with you"
message : "
"
template_name:"shared_project_email_template"
view_data:
project:
name: project.name
url: "#{Settings.siteUrl}/project/#{project._id}?" + [
"project_name=#{project.name}"
"user_first_name=#{project.owner_ref.first_name}"
"new_email=#{email}"
"r=#{project.owner_ref.referal_id}" # Referal
"rs=ci" # referral source = collaborator invite
].join("&")
owner:
first_name: project.owner_ref.first_name
email: project.owner_ref.email
sharelatex_url: Settings.siteUrl
emailer.sendEmail emailOptions
if privlages == 'readAndWrite'
level = {"collaberator_refs":user}
logger.log privileges: "readAndWrite", user: user, project: project, "adding user"
else if privlages == 'readOnly'
level = {"readOnly_refs":user}
logger.log privileges: "readOnly", user: user, project: project, "adding user"
Project.update {_id: project_id}, {$push:level},{},(err)->
projectEntityHandler.flushProjectToThirdPartyDataStore project_id, "", ->
if callback?
callback(user)
emails = mimelib.parseAddresses(email)
email = emails[0].address
User.findOne {'email':email}, (err, user)->
if(!user)
user = new User 'email':email, holdingAccount:true
user.save (err)->
logger.log user: user, 'creating new empty user'
doAdd user
else
doAdd user
removeUserFromProject: (project_id, user_id, callback)->
logger.log user_id: user_id, project_id: project_id, "removing user"
conditions = _id:project_id
update = $pull:{}
update["$pull"] = collaberator_refs:user_id, readOnly_refs:user_id
Project.update conditions, update, {}, (err)->
if err?
logger.err err: err, "problem removing user from project collaberators"
if callback?
callback()
changeUsersPrivlageLevel: (project_id, user_id, newPrivalageLevel)->
@removeUserFromProject project_id, user_id, ()=>
User.findById user_id, (err, user)=>
if user
@addUserToProject project_id, user.email, newPrivalageLevel

View file

@ -0,0 +1,7 @@
EditorUpdatesController = require("../Features/Editor/EditorUpdatesController")
EditorRealTimeController = require("../Features/Editor/EditorRealTimeController")
module.exports = BackgroundTasks =
run: () ->
EditorUpdatesController.listenForUpdatesFromDocumentUpdater()
EditorRealTimeController.listenForEditorEvents()

View file

@ -0,0 +1,11 @@
metrics = require('./Metrics')
module.exports =
log: (req)->
if req.headers["user-agent"]?
userAgent = req.headers["user-agent"].toLowerCase()
if userAgent.indexOf("google") != -1
metrics.inc "crawler.google"
else if userAgent.indexOf("facebook") != -1
metrics.inc "crawler.facebook"
else if userAgent.indexOf("bing") != -1
metrics.inc "crawler.bing"

View file

@ -0,0 +1,122 @@
logger = require 'logger-sharelatex'
fs = require 'fs'
crypto = require 'crypto'
Settings = require('settings-sharelatex')
SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters')
querystring = require('querystring')
fingerprints = {}
Path = require 'path'
jsPath =
if Settings.useMinifiedJs
"/minjs/"
else
"/js/"
logger.log "Generating file fingerprints..."
for path in [
"#{jsPath}libs/require.js",
"#{jsPath}ide.js",
"#{jsPath}main.js",
"#{jsPath}list.js",
"#{jsPath}libs/pdf.js",
"#{jsPath}libs/pdf.worker.js",
"/stylesheets/mainStyle.css"
]
filePath = Path.join __dirname, "../../../", "public#{path}"
content = fs.readFileSync filePath
hash = crypto.createHash("md5").update(content).digest("hex")
logger.log "#{filePath}: #{hash}"
fingerprints[path] = hash
module.exports = (app)->
app.use (req, res, next)->
res.locals.session = req.session
next()
app.use (req, res, next)->
res.locals.jsPath = jsPath
next()
app.use (req, res, next)->
res.locals.settings = Settings
next()
app.use (req, res, next)->
res.locals.getSiteHost = ->
Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2)
next()
app.use (req, res, next)->
res.locals.formatPrivlageLevel = (privlageLevel)->
formatedPrivlages = private:"Private", readOnly:"Read Only", readAndWrite:"Read and Write"
return formatedPrivlages[privlageLevel] || "Private"
next()
app.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?
url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
return url
res.locals.getReferalId = ->
if req.session? and req.session.user? and req.session.user.referal_id
return req.session.user.referal_id
res.locals.getReferalTagLine = ->
tagLines = [
"Roar!"
"Shout about us!"
"Please recommend us"
"Tell the world!"
"Thanks for using ShareLaTeX"
]
return tagLines[Math.floor(Math.random()*tagLines.length)]
res.locals.getRedirAsQueryString = ->
if req.query.redir?
return "?#{querystring.stringify({redir:req.query.redir})}"
return ""
next()
app.use (req, res, next) ->
res.locals.csrfToken = req.session._csrf
next()
app.use (req, res, next)->
res.locals.fingerprint = (path) ->
if fingerprints[path]?
return fingerprints[path]
else
logger.err "No fingerprint for file: #{path}"
return ""
next()
app.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
app.use (req, res, next)->
if req.session.user?
res.locals.mixpanelId = req.session.user._id
res.locals.user =
email: req.session.user.email
first_name: req.session.user.first_name
last_name: req.session.user.last_name
if req.session.justRegistered
res.locals.justRegistered = true
delete req.session.justRegistered
if req.session.justLoggedIn
res.locals.justLoggedIn = true
delete req.session.justLoggedIn
res.locals.mixpanelToken = Settings.analytics?.mixpanel?.token
res.locals.gaToken = Settings.analytics?.ga?.token
res.locals.heapToken = Settings.analytics?.heap?.token
res.locals.tenderUrl = Settings.tenderUrl
next()
app.use (req, res, next) ->
if req.query? and req.query.scribtex_path?
res.locals.lookingForScribtex = true
res.locals.scribtexPath = req.query.scribtex_path
next()

View file

@ -0,0 +1,5 @@
module.exports =
queue:
web_to_tpds_http_requests: "web_to_tpds_http_requests"
tpds_to_web_http_requests: "tpds_to_web_http_requests"

View file

@ -0,0 +1,18 @@
module.exports =
user: (user) ->
if !user._id?
user = {_id : user}
return {
id: user._id
email: user.email
first_name: user.name
last_name: user.name
}
project: (project) ->
if !project._id?
project = {_id: project}
return {
id: project._id
name: project.name
}

View file

@ -0,0 +1,24 @@
StatsD = require('lynx')
settings = require('settings-sharelatex')
statsd = new StatsD('localhost', 8125, {on_error:->})
buildKey = (key)-> "web.#{process.env.NODE_ENV}.#{key}"
module.exports =
set : (key, value, sampleRate = 1)->
statsd.set buildKey(key), value, sampleRate
inc : (key, sampleRate = 1)->
statsd.increment buildKey(key), sampleRate
Timer : class
constructor :(key, sampleRate = 1)->
this.start = new Date()
this.key = buildKey(key)
done:->
timeSpan = new Date - this.start
statsd.timing(this.key, timeSpan, this.sampleRate)
gauge : (key, value, sampleRate = 1)->
statsd.gauge key, value, sampleRate

View file

@ -0,0 +1,5 @@
require("./Monitor/MongoDB").monitor()
exports.logger = require("./Monitor/HTTP").logger

View file

@ -0,0 +1,21 @@
logger = require "logger-sharelatex"
module.exports.logger = (req, res, next) ->
startTime = new Date()
end = res.end
res.end = () ->
end.apply(this, arguments)
logger.log
req:
url: req.originalUrl || req.url
method: req.method
referrer: req.headers['referer'] || req.headers['referrer']
"remote-addr": req.ip || req.socket?.socket?.remoteAddress || req.socket?.remoteAddress
"user-agent": req.headers["user-agent"]
"content-length": req.headers["content-length"]
res:
"content-length": res._headers?["content-length"]
"response-time": new Date() - startTime
"http request"
next()

Some files were not shown because too many files have changed in this diff Show more