Merge pull request #1717 from overleaf/as-decaffeinate-backend

Decaffeinate backend

GitOrigin-RevId: 4ca9f94fc809cab6f47cec8254cacaf1bb3806fa
This commit is contained in:
Alasdair Smith
2019-05-29 10:21:06 +01:00
committed by sharelatex
parent d4eb71b525
commit 0ca81de78c
863 changed files with 177442 additions and 58929 deletions

View File

@@ -1,12 +1,7 @@
app/js
modules/**/app/js
modules/**/scripts
modules/*/index.js
public/js
public/minjs
modules/**/public/js
test/**/js
modules/**/test/**/js
app.js
test/unit_frontend/js
webpack.config.*
karma.conf.js
karma.conf.js

View File

@@ -36,13 +36,6 @@ Thumbs.db
node_modules/*
data/*
app.js
app.js.map
app/js/*
test/unit/js/*
test/unit_frontend/js/*
test/smoke/js/*
test/acceptance/js/*
cookies.txt
requestQueueWorker.js
TpdsWorker.js
@@ -71,10 +64,11 @@ public/stylesheets/ieee-style*.css
public/stylesheets/*.map
public/minjs/
Gemfile.lock
public/js/libs/require*.js
test/unit_frontend/js/
Gemfile.lock
*.swp
.DS_Store
@@ -84,8 +78,3 @@ docker-shared.yml
config/*.coffee
modules/**/Makefile
modules/*/index.js
modules/**/app/js/*
modules/**/test/unit/js/*
modules/**/test/acceptance/js/*

View File

@@ -1,12 +1,7 @@
app/js
modules/**/app/js
modules/**/scripts
modules/*/index.js
public/js
public/minjs
modules/**/public/js
test/**/js
modules/**/test/**/js
app.js
test/unit_frontend/js
webpack.config.*
karma.conf.js
karma.conf.js

View File

@@ -2,7 +2,7 @@
"files.exclude": {
"app/js": true,
"public/js": true,
"test/**/js": true,
"test/unit_frontend/js": true,
"node_modules": true,
"data": true
}

View File

@@ -1,58 +0,0 @@
fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
Settings = require "settings-sharelatex"
require('es6-promise').polyfill()
module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-requirejs'
grunt.loadNpmTasks 'grunt-file-append'
config =
requirejs:
compile:
options:
optimize:"uglify2"
uglify2:
mangle: false
appDir: "public/js"
baseUrl: "./"
dir: "public/minjs"
inlineText: false
generateSourceMaps: true
preserveLicenseComments: false
paths:
"moment": "libs/#{PackageVersions.lib('moment')}"
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
"ace": "#{PackageVersions.lib('ace')}"
"fineuploader": "libs/#{PackageVersions.lib('fineuploader')}"
skipDirOptimize: true
modules: [
{
name: "main",
exclude: ["libraries"]
}, {
name: "ide",
exclude: ["pdfjs-dist/build/pdf", "libraries"]
},{
name: "libraries"
},{
name: "ace/mode-latex"
},{
name: "ace/worker-latex"
}
]
file_append:
default_options: files: [ {
append: '\n//ide.js is complete - used for automated testing'
input: 'public/minjs/ide.js'
output: 'public/minjs/ide.js'
}]
grunt.initConfig config
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"]

86
services/web/Gruntfile.js Normal file
View File

@@ -0,0 +1,86 @@
/* eslint-disable
max-len,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const fs = require('fs')
const PackageVersions = require('./app/src/infrastructure/PackageVersions')
const Settings = require('settings-sharelatex')
require('es6-promise').polyfill()
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-requirejs')
grunt.loadNpmTasks('grunt-file-append')
const config = {
requirejs: {
compile: {
options: {
optimize: 'uglify2',
uglify2: {
mangle: false
},
appDir: 'public/js',
baseUrl: './',
dir: 'public/minjs',
inlineText: false,
generateSourceMaps: true,
preserveLicenseComments: false,
paths: {
moment: `libs/${PackageVersions.lib('moment')}`,
mathjax: '/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML',
'pdfjs-dist/build/pdf': `libs/${PackageVersions.lib('pdfjs')}/pdf`,
ace: `${PackageVersions.lib('ace')}`,
fineuploader: `libs/${PackageVersions.lib('fineuploader')}`
},
skipDirOptimize: true,
modules: [
{
name: 'main',
exclude: ['libraries']
},
{
name: 'ide',
exclude: ['pdfjs-dist/build/pdf', 'libraries']
},
{
name: 'libraries'
},
{
name: 'ace/mode-latex'
},
{
name: 'ace/worker-latex'
}
]
}
}
},
file_append: {
default_options: {
files: [
{
append: '\n//ide.js is complete - used for automated testing',
input: 'public/minjs/ide.js',
output: 'public/minjs/ide.js'
}
]
}
}
}
grunt.initConfig(config)
return grunt.registerTask(
'compile:minify',
'Concat and minify the client side js',
['requirejs', 'file_append']
)
}

View File

@@ -16,23 +16,16 @@ MODULE_DIRS := $(shell find modules -mindepth 1 -maxdepth 1 -type d -not -name '
MODULE_MAKEFILES := $(MODULE_DIRS:=/Makefile)
MODULE_NAME=$(shell basename $(MODULE))
COFFEE := node_modules/.bin/coffee -m $(COFFEE_OPTIONS)
COFFEE := node_modules/.bin/coffee $(COFFEE_OPTIONS)
BABEL := node_modules/.bin/babel
GRUNT := node_modules/.bin/grunt
LESSC := node_modules/.bin/lessc
CLEANCSS := node_modules/.bin/cleancss
APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee')
FRONT_END_SRC_FILES := $(shell find public/src -name '*.js')
TEST_COFFEE_FILES := $(shell find test/*/coffee -name '*.coffee')
TEST_SRC_FILES := $(shell find test/*/src -name '*.js')
MODULE_MAIN_SRC_FILES := $(shell find modules -type f -wholename '*main/index.js')
MODULE_IDE_SRC_FILES := $(shell find modules -type f -wholename '*ide/index.js')
COFFEE_FILES := app.coffee $(APP_COFFEE_FILES) $(FRONT_END_COFFEE_FILES) $(TEST_COFFEE_FILES)
SRC_FILES := $(FRONT_END_SRC_FILES) $(TEST_SRC_FILES)
JS_FILES := $(subst coffee,js,$(COFFEE_FILES))
OUTPUT_SRC_FILES := $(subst src,js,$(SRC_FILES))
SRC_FILES := $(shell find public/src -name '*.js')
DIST_FILES := $(subst src,js,$(SRC_FILES))
MAIN_SRC_FILES := $(shell find modules -type f -wholename '*main/index.js')
IDE_SRC_FILES := $(shell find modules -type f -wholename '*ide/index.js')
LESS_FILES := $(shell find public/stylesheets -name '*.less')
LESSC_COMMON_FLAGS := --source-map --autoprefix="last 2 versions, ie >= 10"
CLEANCSS_FLAGS := --s0 --source-map
@@ -49,35 +42,15 @@ CSS_OL_IEEE_FILE := public/stylesheets/ieee-style.css
CSS_FILES := $(CSS_SL_FILE) $(CSS_OL_FILE) $(CSS_OL_LIGHT_FILE) $(CSS_OL_IEEE_FILE)
# The automatic variable $(@D) is the target directory name
app.js: app.coffee
$(COFFEE) --compile -o $(@D) $<
app/js/%.js: app/coffee/%.coffee
@mkdir -p $(@D)
$(COFFEE) --compile -o $(@D) $<
public/js/%.js: public/src/%.js
@mkdir -p $(@D)
$(BABEL) $< --out-file $@
test/unit/js/%.js: test/unit/coffee/%.coffee
@mkdir -p $(@D)
$(COFFEE) --compile -o $(@D) $<
test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
@mkdir -p $(@D)
$(COFFEE) --compile -o $(@D) $<
test/unit_frontend/js/%.js: test/unit_frontend/src/%.js
@mkdir -p $(@D)
$(BABEL) $< --out-file $@
test/smoke/js/%.js: test/smoke/coffee/%.coffee
@mkdir -p $(@D)
$(COFFEE) --compile -o $(@D) $<
public/js/ide.js: public/src/ide.js $(MODULE_IDE_SRC_FILES)
public/js/ide.js: public/src/ide.js $(IDE_SRC_FILES)
@echo Compiling and injecting module includes into public/js/ide.js
@INCLUDES=""; \
for dir in modules/*; \
@@ -92,7 +65,7 @@ public/js/ide.js: public/src/ide.js $(MODULE_IDE_SRC_FILES)
sed -e s=\'__IDE_CLIENTSIDE_INCLUDES__\'=$$INCLUDES= \
> $@
public/js/main.js: public/src/main.js $(MODULE_MAIN_SRC_FILES)
public/js/main.js: public/src/main.js $(MAIN_SRC_FILES)
@echo Compiling and injecting module includes into public/js/main.js
@INCLUDES=""; \
for dir in modules/*; \
@@ -114,7 +87,7 @@ css_full: $(CSS_FILES)
css: $(CSS_OL_FILE)
minify: $(CSS_FILES) $(JS_FILES) $(OUTPUT_SRC_FILES)
minify: $(CSS_FILES) $(DIST_FILES)
$(GRUNT) compile:minify
$(MAKE) minify_css
$(MAKE) minify_es
@@ -128,18 +101,11 @@ minify_css: $(CSS_FILES)
minify_es:
npm -q run webpack:production
compile: compile_app $(OUTPUT_SRC_FILES) css public/js/main.js public/js/ide.js
compile_app: $(JS_FILES)
compile: $(DIST_FILES) css public/js/main.js public/js/ide.js
@$(MAKE) compile_modules
compile_full:
$(COFFEE) -c -p app.coffee > app.js
$(COFFEE) -o app/js -c app/coffee
$(BABEL) public/src --out-dir public/js
$(COFFEE) -o test/acceptance/js -c test/acceptance/coffee
$(COFFEE) -o test/smoke/js -c test/smoke/coffee
$(COFFEE) -o test/unit/js -c test/unit/coffee
$(BABEL) test/unit_frontend/src --out-dir test/unit_frontend/js
rm -f public/js/ide.js public/js/main.js # We need to generate ide.js, main.js manually later
$(MAKE) css_full
@@ -149,9 +115,6 @@ compile_full:
compile_css_full:
$(MAKE) css_full
compile_module:
cd modules/$(MODULE_NAME) && $(MAKE) compile
compile_modules: $(MODULE_MAKEFILES)
@set -e; \
for dir in $(MODULE_DIRS); \
@@ -184,33 +147,18 @@ $(MODULE_MAKEFILES): Makefile.module
cp Makefile.module $$makefile; \
done
clean: clean_app clean_frontend clean_css clean_tests clean_modules
clean_app:
rm -f app.js app.js.map
rm -rf app/js
clean: clean_frontend clean_css clean_tests
clean_frontend:
rm -rf public/js/{analytics,directives,es,filters,ide,main,modules,services,utils}
rm -f public/js/*.{js,map}
clean_tests:
rm -rf test/unit/js
rm -rf test/unit_frontend/js
rm -rf test/acceptance/js
clean_modules:
for dir in modules/*; \
do \
rm -f $$dir/index.js; \
rm -rf $$dir/app/js; \
rm -rf $$dir/test/unit/js; \
rm -rf $$dir/test/acceptance/js; \
done
clean_css:
rm -f public/stylesheets/*.css*
clean_tests:
rm -rf test/unit_frontend/js
clean_ci:
$(DOCKER_COMPOSE) down -v -t 0
docker container list | grep 'days ago' | cut -d ' ' -f 1 - | xargs -r docker container stop
@@ -219,12 +167,12 @@ clean_ci:
test: test_unit test_frontend test_acceptance
test_module: compile_module test_unit_module_run test_acceptance_module_run
test_module: test_unit_module_run test_acceptance_module_run
test_unit:
@[ ! -d test/unit ] && echo "web has no unit tests" || COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit
test_unit_module: compile_module test_unit_module_run
test_unit_module: test_unit_module_run
test_unit_module_run:
COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit bin/unit_test_module $(MODULE_NAME) --grep=$(MOCHA_GREP)
@@ -241,17 +189,17 @@ test_frontend_run:
test_frontend_build_run: build_test_frontend test_frontend_run
test_acceptance: compile test_acceptance_app_run test_acceptance_modules_run
test_acceptance: test_acceptance_app_run test_acceptance_modules_run
test_acceptance_app: compile test_acceptance_app_run
test_acceptance_app: test_acceptance_app_run
test_acceptance_module: compile_module test_acceptance_module_run
test_acceptance_module: test_acceptance_module_run
test_acceptance_run: test_acceptance_app_run test_acceptance_modules_run
test_acceptance_app_run:
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_acceptance npm -q run test:acceptance:run_dir test/acceptance/js
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_acceptance npm -q run test:acceptance:run_dir test/acceptance/src
COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0
test_acceptance_modules_run:

View File

@@ -1,22 +1,16 @@
MODULE_NAME := $(notdir $(shell pwd))
MODULE_DIR := modules/$(MODULE_NAME)
PROJECT_NAME = web
COFFEE := ../../node_modules/.bin/coffee
BABEL := ../../node_modules/.bin/babel
APP_COFFEE_FILES := $(shell [ -e app/coffee ] && find app/coffee -name '*.coffee') \
$(shell [ -e test/unit/coffee ] && find test/unit/coffee -name '*.coffee') \
$(shell [ -e test/acceptance/coffee ] && find test/acceptance/coffee -name '*.coffee')
APP_JS_FILES := $(subst coffee,js,$(APP_COFFEE_FILES))
IDE_SRC_FILES := $(shell [ -e public/src/ide ] && find public/src/ide -name '*.js')
IDE_OUTPUT_FILES := $(subst public/src/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_SRC_FILES))
IDE_DIST_FILES := $(subst public/src/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_SRC_FILES))
IDE_TEST_SRC_FILES := $(shell [ -e test/unit_frontend/src/ide ] && find test/unit_frontend/src/ide -name '*.js')
IDE_TEST_OUTPUT_FILES := $(subst test/unit_frontend/src/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_SRC_FILES))
IDE_TEST_DIST_FILES := $(subst test/unit_frontend/src/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_SRC_FILES))
MAIN_SRC_FILES := $(shell [ -e public/src/main ] && find public/src/main -name '*.js')
MAIN_OUTPUT_FILES := $(subst public/src/main,../../public/js/main/$(MODULE_NAME),$(MAIN_SRC_FILES))
MAIN_DIST_FILES := $(subst public/src/main,../../public/js/main/$(MODULE_NAME),$(MAIN_SRC_FILES))
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
DOCKER_COMPOSE_MODULE_FLAGS := ${DOCKER_COMPOSE_FLAGS} -f $(MODULE_DIR)/docker-compose.yml
@@ -28,18 +22,6 @@ DOCKER_COMPOSE := cd ../../ && \
MOCHA_GREP=${MOCHA_GREP} \
docker-compose ${DOCKER_COMPOSE_MODULE_FLAGS}
app/js/%.js: app/coffee/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
test/unit/js/%.js: test/unit/coffee/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
@mkdir -p $(dir $@)
$(COFFEE) --compile --print $< > $@
../../test/unit_frontend/js/ide/$(MODULE_NAME)/%.js: test/unit_frontend/src/ide/%.js
@mkdir -p $(dir $@)
$(BABEL) $< --out-file $@
@@ -52,20 +34,14 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
@mkdir -p $(dir $@)
$(BABEL) $< --out-file $@
index.js: index.coffee
$(COFFEE) --compile --print $< > $@
compile: $(APP_JS_FILES) $(IDE_OUTPUT_FILES) $(MAIN_OUTPUT_FILES) $(IDE_TEST_OUTPUT_FILES) index.js
compile: $(IDE_DIST_FILES) $(MAIN_DIST_FILES) $(IDE_TEST_DIST_FILES)
@echo > /dev/null
compile_full:
if [ -e app/coffee ]; then $(COFFEE) -o app/js -c app/coffee; fi
if [ -e test/unit/coffee ]; then $(COFFEE) -o test/unit/js -c test/unit/coffee; fi
if [ -e test/acceptance/coffee ]; then $(COFFEE) -o test/acceptance/js -c test/acceptance/coffee; fi
if [ -e public/src/ide ]; then $(BABEL) public/src/ide --out-dir ../../public/js/ide/$(MODULE_NAME); fi
if [ -e public/src/main ]; then $(BABEL) public/src/main --out-dir ../../public/js/main/$(MODULE_NAME); fi
if [ -e test/unit_frontend/src/ide ]; then $(BABEL) test/unit_frontend/src/ide --out-dir ../../test/unit_frontend/js/ide/$(MODULE_NAME); fi
@$(MAKE) compile
@$(MAKE) compile # Anything else missed
test_acceptance:
${DOCKER_COMPOSE} run --rm test_acceptance npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/js
${DOCKER_COMPOSE} run --rm test_acceptance npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/src

View File

@@ -1,41 +0,0 @@
metrics = require("metrics-sharelatex")
metrics.initialize(process.env['METRICS_APP_NAME'] or "web")
Settings = require('settings-sharelatex')
logger = require 'logger-sharelatex'
logger.initialize(process.env['METRICS_APP_NAME'] or "web")
logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user
logger.logger.serializers.docs = require("./app/js/infrastructure/LoggerSerializers").docs
logger.logger.serializers.files = require("./app/js/infrastructure/LoggerSerializers").files
logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project
if Settings.sentry?.dsn?
logger.initializeErrorReporting(Settings.sentry.dsn)
metrics.memory.monitor(logger)
Server = require("./app/js/infrastructure/Server")
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
if Settings.catchErrors
process.removeAllListeners "uncaughtException"
process.on "uncaughtException", (error) ->
logger.error err: error, "uncaughtException"
port = Settings.port or Settings.internal?.web?.port or 3000
host = Settings.internal.web.host or "localhost"
if !module.parent # Called directly
Server.server.listen port, host, ->
logger.info "web starting up, listening on #{host}:#{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}"
module.exports = Server.server

77
services/web/app.js Normal file
View File

@@ -0,0 +1,77 @@
/* eslint-disable
max-len,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const metrics = require('metrics-sharelatex')
metrics.initialize(process.env['METRICS_APP_NAME'] || 'web')
const Settings = require('settings-sharelatex')
const logger = require('logger-sharelatex')
logger.initialize(process.env['METRICS_APP_NAME'] || 'web')
logger.logger.serializers.user = require('./app/src/infrastructure/LoggerSerializers').user
logger.logger.serializers.docs = require('./app/src/infrastructure/LoggerSerializers').docs
logger.logger.serializers.files = require('./app/src/infrastructure/LoggerSerializers').files
logger.logger.serializers.project = require('./app/src/infrastructure/LoggerSerializers').project
if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) {
logger.initializeErrorReporting(Settings.sentry.dsn)
}
metrics.memory.monitor(logger)
const Server = require('./app/src/infrastructure/Server')
const { 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')
if (Settings.catchErrors) {
process.removeAllListeners('uncaughtException')
process.on('uncaughtException', error =>
logger.error({ err: error }, 'uncaughtException')
)
}
const port =
Settings.port ||
__guard__(
Settings.internal != null ? Settings.internal.web : undefined,
x => x.port
) ||
3000
const host = Settings.internal.web.host || 'localhost'
if (!module.parent) {
// Called directly
Server.server.listen(port, host, function() {
logger.info(`web starting up, listening on ${host}:${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)
return logger.info(`Running as group: ${argv.group}`)
}
})
}
module.exports = Server.server
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}

View File

@@ -1,42 +0,0 @@
AnalyticsManager = require "./AnalyticsManager"
Errors = require "../Errors/Errors"
AuthenticationController = require("../Authentication/AuthenticationController")
InstitutionsAPI = require("../Institutions/InstitutionsAPI")
GeoIpLookup = require '../../infrastructure/GeoIpLookup'
module.exports = AnalyticsController =
updateEditingSession: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
projectId = req.params.projectId
countryCode = null
if userId?
GeoIpLookup.getDetails req.ip, (err, geoDetails) ->
if geoDetails?.country_code? and geoDetails.country_code != ""
countryCode = geoDetails.country_code
AnalyticsManager.updateEditingSession userId, projectId, countryCode, (error) ->
respondWith(error, res, next)
else
res.send 204
recordEvent: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req) or req.sessionID
AnalyticsManager.recordEvent user_id, req.params.event, req.body, (error) ->
respondWith(error, res, next)
licences: (req, res, next) ->
{resource_id, start_date, end_date, lag} = req.query
InstitutionsAPI.getInstitutionLicences resource_id, start_date, end_date, lag, (error, licences) ->
if error?
next(error)
else
res.send licences
respondWith = (error, res, next) ->
if error instanceof Errors.ServiceNotConfiguredError
# ignore, no-op
res.send(204)
else if error?
next(error)
else
res.send 204

View File

@@ -1,109 +0,0 @@
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
_ = require "underscore"
request = require "requestretry"
Errors = require '../Errors/Errors'
makeFaultTolerantRequest = (userId, options, callback) ->
if userId+"" == settings.smokeTest?.userId+""
return callback()
options = Object.assign(options, {
delayStrategy: exponentialBackoffStrategy()
timeout: 30000
})
if settings.overleaf?
options.qs = Object.assign({}, options.qs, { fromV2: 1 })
makeRequest options, (err) ->
if err?
logger.err { err: err }, 'Request to analytics failed'
callback() # Do not wait for all the attempts
makeRequest = (opts, callback)->
if settings.apis?.analytics?.url?
urlPath = opts.url
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
request opts, callback
else
callback(new Errors.ServiceNotConfiguredError('Analytics service not configured'))
# Set an exponential backoff to retry calls to analytics. First retry will
# happen after 4s, then 8, 16, 32, 64...
exponentialBackoffStrategy = () ->
attempts = 1 # This won't be called until there has been 1 failure
() ->
attempts += 1
exponentialBackoffDelay(attempts)
exponentialBackoffDelay = (attempts) ->
delay = 2 ** attempts * 1000
logger.warn "Error comunicating with the analytics service. " +
"Will try again attempt #{attempts} in #{delay}ms"
delay
module.exports =
identifyUser: (user_id, old_user_id, callback = (error)->)->
opts =
body:
old_user_id:old_user_id
json:true
method:"POST"
timeout:1000
url: "/user/#{user_id}/identify"
makeRequest opts, callback
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
opts =
body:
event:event
segmentation:segmentation
json:true
method:"POST"
url: "/user/#{user_id}/event"
maxAttempts: 7 # Give up after ~ 8min
makeFaultTolerantRequest user_id, opts, callback
updateEditingSession: (userId, projectId, countryCode, callback = (error) ->) ->
query =
userId: userId
projectId: projectId
if countryCode
query.countryCode = countryCode
opts =
method: "PUT"
url: "/editingSession"
qs: query
maxAttempts: 6 # Give up after ~ 4min
makeFaultTolerantRequest userId, opts, callback
getLastOccurrence: (user_id, event, callback = (error) ->) ->
opts =
body:
event:event
json:true
method:"POST"
timeout:1000
url: "/user/#{user_id}/event/last_occurrence"
makeRequest opts, (err, response, body)->
if err?
console.log response, opts
logger.err {user_id, err}, "error getting last occurance of event"
return callback err
else
return callback null, body

View File

@@ -1,20 +0,0 @@
settings = require "settings-sharelatex"
Errors = require '../Errors/Errors'
httpProxy = require 'express-http-proxy'
URL = require 'url'
module.exports =
call: (basePath) ->
analyticsUrl = settings?.apis?.analytics?.url
if analyticsUrl?
httpProxy(analyticsUrl,
proxyReqPathResolver: (req) ->
requestPath = URL.parse(req.url).path
"#{basePath}#{requestPath}"
proxyReqOptDecorator: (proxyReqOpts, srcReq) ->
proxyReqOpts.headers = {} # unset all headers
proxyReqOpts
)
else
(req, res, next) ->
next(new Errors.ServiceNotConfiguredError('Analytics service not configured'))

View File

@@ -1,26 +0,0 @@
AuthenticationController = require './../Authentication/AuthenticationController'
AnalyticsController = require('./AnalyticsController')
AnalyticsProxy = require('./AnalyticsProxy')
module.exports =
apply: (webRouter, privateApiRouter, publicApiRouter) ->
webRouter.post '/event/:event', AnalyticsController.recordEvent
webRouter.put '/editingSession/:projectId',
AnalyticsController.updateEditingSession
publicApiRouter.use '/analytics/graphs',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/graphs')
publicApiRouter.use '/analytics/recentTeamActivity',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/recentTeamActivity')
publicApiRouter.use '/analytics/recentV1TemplateIdsActivity',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/recentV1TemplateIdsActivity')
publicApiRouter.use '/analytics/uniExternalCollaboration',
AuthenticationController.httpAuth,
AnalyticsProxy.call('/uniExternalCollaboration')

View File

@@ -1,24 +0,0 @@
AnnouncementsHandler = require("./AnnouncementsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
settings = require("settings-sharelatex")
module.exports =
getUndreadAnnouncements: (req, res, next)->
if !settings?.apis?.analytics?.url? or !settings.apis.blog.url?
return res.json []
user = AuthenticationController.getSessionUser(req)
logger.log {user_id:user?._id}, "getting unread announcements"
AnnouncementsHandler.getUnreadAnnouncements user, (err, announcements)->
if err?
logger.err {err:err, user_id:user._id}, "unable to get unread announcements"
next(err)
else
res.json announcements

View File

@@ -1,67 +0,0 @@
AnalyticsManager = require("../Analytics/AnalyticsManager")
BlogHandler = require("../Blog/BlogHandler")
logger = require("logger-sharelatex")
settings = require("settings-sharelatex")
async = require("async")
_ = require("lodash")
module.exports = AnnouncementsHandler =
_domainSpecificAnnouncements : (email)->
domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)->
matches = _.filter domainAnnouncment.domains, (domain)->
return email.indexOf(domain) != -1
return matches.length > 0 and domainAnnouncment.id?
return domainSpecific or []
getUnreadAnnouncements : (user, callback = (err, announcements)->)->
if !user? and !user._id?
return callback("user not supplied")
timestamp = user._id.toString().substring(0,8)
userSignupDate = new Date( parseInt( timestamp, 16 ) * 1000 )
async.parallel {
lastEvent: (cb)->
AnalyticsManager.getLastOccurrence user._id, "announcement-alert-dismissed", cb
announcements: (cb)->
BlogHandler.getLatestAnnouncements cb
}, (err, results)->
if err?
logger.err err:err, user_id:user._id, "error getting unread announcements"
return callback(err)
domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email)
domainSpecific = _.map domainSpecific, (domainAnnouncment)->
try
domainAnnouncment.date = new Date(domainAnnouncment.date)
return domainAnnouncment
catch e
return callback(e)
announcements = results.announcements
announcements = _.union announcements, domainSpecific
announcements = _.sortBy(announcements, "date").reverse()
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
announcementIndex = _.findIndex announcements, (announcement)->
announcement.id == lastSeenBlogId
announcements = _.map announcements, (announcement, index)->
if announcement.date < userSignupDate
read = true
else if announcementIndex == -1
read = false
else if index >= announcementIndex
read = true
else
read = false
announcement.read = read
return announcement
logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements"
callback null, announcements

View File

@@ -1,321 +0,0 @@
AuthenticationManager = require ("./AuthenticationManager")
LoginRateLimiter = require("../Security/LoginRateLimiter")
UserUpdater = require "../User/UserUpdater"
Metrics = require('metrics-sharelatex')
logger = require("logger-sharelatex")
querystring = require('querystring')
Url = require("url")
Settings = require "settings-sharelatex"
basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
passport = require 'passport'
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
SudoModeHandler = require '../SudoMode/SudoModeHandler'
V1Api = require "../V1/V1Api"
{User} = require "../../models/User"
{ URL } = require('url')
module.exports = AuthenticationController =
serializeUser: (user, callback) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
staffAccess: user.staffAccess
email: user.email
referal_id: user.referal_id
session_created: (new Date()).toISOString()
ip_address: user._login_req_ip
must_reconfirm: user.must_reconfirm
v1_id: user.overleaf?.id
callback(null, lightUser)
deserializeUser: (user, cb) ->
cb(null, user)
afterLoginSessionSetup: (req, user, callback=(err)->) ->
req.login user, (err) ->
if err?
logger.err {user_id: user._id, err}, "error from req.login"
return callback(err)
# Regenerate the session to get a new sessionID (cookie value) to
# protect against session fixation attacks
oldSession = req.session
req.session.destroy (err) ->
if err?
logger.err {user_id: user._id, err}, "error when trying to destroy old session"
return callback(err)
req.sessionStore.generate(req)
for key, value of oldSession
req.session[key] = value unless key == '__tmp'
# copy to the old `session.user` location, for backward-comptability
req.session.user = req.session.passport.user
req.session.save (err) ->
if err?
logger.err {user_id: user._id}, "error saving regenerated session after login"
return callback(err)
UserSessionsManager.trackSession(user, req.sessionID, () ->)
callback(null)
passportLogin: (req, res, next) ->
# This function is middleware which wraps the passport.authenticate middleware,
# so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
# and send a `{redir: ""}` response on success
passport.authenticate('local', (err, user, info) ->
if err?
return next(err)
if user # `user` is either a user object or false
AuthenticationController.finishLogin(user, req, res, next)
else
if info.redir?
res.json {redir: info.redir}
else
res.json message: info
)(req, res, next)
finishLogin: (user, req, res, next) ->
return res.redirect('/login') if user == false # OAuth2 'state' mismatch
if user.must_reconfirm
AuthenticationController._redirectToReconfirmPage req, res, user
else
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
AuthenticationController._loginAsyncHandlers(req, user)
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
if err?
return next(err)
SudoModeHandler.activateSudoMode user._id, (err) ->
if err?
logger.err {err, user_id: user._id}, "Error activating Sudo Mode on login, continuing"
AuthenticationController._clearRedirectFromSession(req)
if req.headers?['accept']?.match(/^application\/json.*$/)
res.json {redir: redir}
else
res.redirect(redir)
doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase()
Modules = require "../../infrastructure/Modules"
Modules.hooks.fire 'preDoPassportLogin', req, email, (err, infoList) ->
return next(err) if err?
info = infoList.find((i) => i?)
if info?
return done(null, false, info)
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return done(error) if error?
if user?
# async actions
return done(null, user)
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
return done(
null,
false,
{text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}
)
_loginAsyncHandlers: (req, user) ->
UserHandler.setupLoginData(user, ()->)
LoginRateLimiter.recordSuccessfulLogin(user.email)
AuthenticationController._recordSuccessfulLogin(user._id)
AuthenticationController.ipMatchCheck(req, user)
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
Analytics.identifyUser(user._id, req.sessionID)
logger.log email: user.email, user_id: user._id.toString(), "successful log in"
req.session.justLoggedIn = true
# capture the request ip for use when creating the session
user._login_req_ip = req.ip
ipMatchCheck: (req, user) ->
if req.ip != user.lastLoginIp
NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip)
UserUpdater.updateUser user._id.toString(), {
$set: { "lastLoginIp": req.ip }
}
setInSessionUser: (req, props) ->
for key, value of props
if req?.session?.passport?.user?
req.session.passport.user[key] = value
if req?.session?.user?
req.session.user[key] = value
isUserLoggedIn: (req) ->
user_id = AuthenticationController.getLoggedInUserId(req)
return (user_id not in [null, undefined, false])
# TODO: perhaps should produce an error if the current user is not present
getLoggedInUserId: (req) ->
user = AuthenticationController.getSessionUser(req)
if user
return user._id
else
return null
getLoggedInUserV1Id: (req) ->
user = AuthenticationController.getSessionUser(req)
if user?.v1_id?
return user.v1_id
else
return null
getSessionUser: (req) ->
if req?.session?.user?
return req.session.user
else if req?.session?.passport?.user
return req.session.passport.user
else
return null
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
if !AuthenticationController.isUserLoggedIn(req)
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
req.user = AuthenticationController.getSessionUser(req)
next()
return doRequest
# access tokens might be associated with user stubs if the user is
# not yet migrated to v2. if api can work with user stubs then set
# allowUserStub true when adding middleware to route.
requireOauth: (allowUserStub=false) ->
# require this here because module may not be included in some versions
Oauth2Server = require "../../../../modules/oauth2-server/app/js/Oauth2Server"
return (req, res, next = (error) ->) ->
request = new Oauth2Server.Request(req)
response = new Oauth2Server.Response(res)
Oauth2Server.server.authenticate request, response, {}, (err, token) ->
if err?
# use a 401 status code for malformed header for git-bridge
err.code = 401 if err.code == 400 and err.message == 'Invalid request: malformed authorization header'
# fall back to v1 on invalid token
return AuthenticationController._requireOauthV1Fallback req, res, next if err.code == 401
# send all other errors
return res.status(err.code).json({error: err.name, error_description: err.message})
return res.sendStatus 401 if token.user.constructor.modelName == "UserStub" and !allowUserStub
req.oauth =
access_token: token.accessToken
req.oauth_token = token
req.oauth_user = token.user
return next()
_requireOauthV1Fallback: (req, res, next) ->
return res.sendStatus 401 unless req.token?
options =
expectedStatusCodes: [401]
json: token: req.token
method: "POST"
uri: "/api/v1/sharelatex/oauth_authorize"
V1Api.request options, (error, response, body) ->
return next(error) if error?
return res.status(401).json({error: "invalid_token"}) unless body?.user_profile?.id
User.findOne { "overleaf.id": body.user_profile.id }, (error, user) ->
return next(error) if error?
return res.status(401).json({error: "invalid_token"}) unless user?
req.oauth =
access_token: body.access_token
req.oauth_user = user
next()
_globalLoginWhitelist: []
addEndpointToLoginWhitelist: (endpoint) ->
AuthenticationController._globalLoginWhitelist.push endpoint
requireGlobalLogin: (req, res, next) ->
if req._parsedUrl.pathname in AuthenticationController._globalLoginWhitelist
return next()
if req.headers['authorization']?
return AuthenticationController.httpAuth(req, res, next)
else if AuthenticationController.isUserLoggedIn(req)
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
AuthenticationController.setRedirectInSession(req)
return res.redirect "/login"
httpAuth: basicAuth (user, pass)->
isValid = Settings.httpAuthUsers[user] == pass
if !isValid
logger.err user:user, pass:pass, "invalid login details"
return isValid
setRedirectInSession: (req, value) ->
if !value?
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
if (
req.session? &&
!/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
!/^.*\.(png|jpeg|svg)$/.test(value)
)
safePath = AuthenticationController._getSafeRedirectPath(value)
req.session.postLoginRedirect = safePath
_redirectToLoginOrRegisterPage: (req, res)->
if (req.query.zipUrl? or req.query.project_name? or req.path == '/user/subscription/new')
return AuthenticationController._redirectToRegisterPage(req, res)
else
AuthenticationController._redirectToLoginPage(req, res)
_redirectToLoginPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to login page"
AuthenticationController.setRedirectInSession(req)
url = "/login?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
_redirectToReconfirmPage: (req, res, user) ->
logger.log url: req.url, "user needs to reconfirm so redirecting to reconfirm page"
req.session.reconfirm_email = user?.email
redir = "/user/reconfirm"
if req.headers?['accept']?.match(/^application\/json.*$/)
res.json {redir: redir}
else
res.redirect redir
_redirectToRegisterPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to register page"
AuthenticationController.setRedirectInSession(req)
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()
_getRedirectFromSession: (req) ->
value = req?.session?.postLoginRedirect
safePath = AuthenticationController._getSafeRedirectPath(value) if value
return safePath || null
_clearRedirectFromSession: (req) ->
if req.session?
delete req.session.postLoginRedirect
_getSafeRedirectPath: (value) ->
baseURL = Settings.siteUrl # base URL is required to construct URL from path
url = new URL(value, baseURL)
safePath = "#{url.pathname}#{url.search}#{url.hash}"
safePath = undefined if safePath == '/'
safePath

View File

@@ -1,136 +0,0 @@
Settings = require "settings-sharelatex"
User = require("../../models/User").User
{db, ObjectId} = require("../../infrastructure/mongojs")
crypto = require 'crypto'
bcrypt = require 'bcrypt'
EmailHelper = require("../Helpers/EmailHelper")
Errors = require("../Errors/Errors")
UserGetter = require("../User/UserGetter")
V1Handler = require '../V1/V1Handler'
BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
_checkWriteResult = (result, callback = (error, updated) ->) ->
# for MongoDB
if result and result.nModified == 1
callback(null, true)
else
callback(null, false)
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
AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
return callback(err) if err?
callback null, user
else
callback null, null
else
callback null, null
else
callback null, null
validateEmail: (email) ->
parsed = EmailHelper.parseEmail(email)
if !parsed?
return { message: 'email not valid' }
return null
# validates a password based on a similar set of rules to `complexPassword.js` on the frontend
# note that `passfield.js` enforces more rules than this, but these are the most commonly set.
# returns null on success, or an error string.
validatePassword: (password) ->
return { message: 'password not set' } unless password?
allowAnyChars = Settings.passwordStrengthOptions?.allowAnyChars == true
min = Settings.passwordStrengthOptions?.length?.min || 6
max = Settings.passwordStrengthOptions?.length?.max || 72
# we don't support passwords > 72 characters in length, because bcrypt truncates them
max = 72 if max > 72
return { message: 'password is too short' } unless password.length >= min
return { message: 'password is too long' } unless password.length <= max
return { message: 'password contains an invalid character' } unless allowAnyChars || AuthenticationManager._passwordCharactersAreValid(password)
return null
setUserPassword: (user_id, password, callback = (error, changed) ->) ->
validation = @validatePassword(password)
return callback(validation.message) if validation?
UserGetter.getUser user_id, { email:1, overleaf: 1 }, (error, user) ->
return callback(error) if error?
v1IdExists = user.overleaf?.id?
if v1IdExists and Settings.overleaf? # v2 user in v2
# v2 user in v2, change password in v1
AuthenticationManager.setUserPasswordInV1(user.overleaf.id, password, callback)
else if v1IdExists and !Settings.overleaf?
# v2 user in SL
return callback(new Errors.NotInV2Error("Password Reset Attempt"))
else if !v1IdExists and !Settings.overleaf?
# SL user in SL, change password in SL
AuthenticationManager.setUserPasswordInV2(user_id, password, callback)
else if !v1IdExists and Settings.overleaf?
# SL user in v2, should not happen
return callback(new Errors.SLInV2Error("Password Reset Attempt"))
else
return callback(new Error("Password Reset Attempt Failed"))
checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
# Temporarily disable this function, TODO: re-enable this
if Settings?.security?.disableBcryptRoundsUpgrades
return callback()
# check current number of rounds and rehash if necessary
currentRounds = bcrypt.getRounds hashedPassword
if currentRounds < BCRYPT_ROUNDS
AuthenticationManager.setUserPassword user._id, password, callback
else
callback()
setUserPasswordInV2: (user_id, password, callback) ->
validation = @validatePassword(password)
return callback(validation.message) if validation?
minorVersion = 'a'
bcrypt.genSalt BCRYPT_ROUNDS, minorVersion, (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
}, (updateError, result)->
return callback(updateError) if updateError?
_checkWriteResult(result, callback)
)
setUserPasswordInV1: (v1_user_id, password, callback) ->
validation = @validatePassword(password)
return callback(validation.message) if validation?
V1Handler.doPasswordReset v1_user_id, password, (error, reset)->
return callback(error) if error?
return callback(error, reset)
_passwordCharactersAreValid: (password) ->
digits = Settings.passwordStrengthOptions?.chars?.digits || '1234567890'
letters = Settings.passwordStrengthOptions?.chars?.letters || 'abcdefghijklmnopqrstuvwxyz'
letters_up = Settings.passwordStrengthOptions?.chars?.letters_up || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
symbols = Settings.passwordStrengthOptions?.chars?.symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,'
for charIndex in [0..password.length - 1]
return false unless digits.indexOf(password[charIndex]) > -1 or
letters.indexOf(password[charIndex]) > -1 or
letters_up.indexOf(password[charIndex]) > -1 or
symbols.indexOf(password[charIndex]) > -1
return true

View File

@@ -1,118 +0,0 @@
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
ProjectGetter = require('../Project/ProjectGetter')
User = require("../../models/User").User
PrivilegeLevels = require("./PrivilegeLevels")
PublicAccessLevels = require("./PublicAccessLevels")
Errors = require("../Errors/Errors")
ObjectId = require("mongojs").ObjectId
TokenAccessHandler = require('../TokenAccess/TokenAccessHandler')
module.exports = AuthorizationManager =
getPublicAccessLevel: (project_id, callback=(err, level)->) ->
if !ObjectId.isValid(project_id)
return callback(new Error("invalid project id"))
# Note, the Project property in the DB is `publicAccesLevel`, without the second `s`
ProjectGetter.getProject project_id, publicAccesLevel: 1, (error, project) ->
return callback(error) if error?
if !project?
return callback new Errors.NotFoundError("no project found with id #{project_id}")
callback null, project.publicAccesLevel
# Get the privilege level that the user has for the project
# Returns:
# * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has
# access. false if the user does not have access
# * becausePublic: true if the access level is only because the project is public.
# * becauseSiteAdmin: true if access level is only because user is admin
getPrivilegeLevelForProject: (
user_id, project_id, token,
callback = (error, privilegeLevel, becausePublic, becauseSiteAdmin) ->
) ->
if !user_id?
# User is Anonymous, Try Token-based access
AuthorizationManager.getPublicAccessLevel project_id, (err, publicAccessLevel) ->
return callback(err) if err?
if publicAccessLevel == PublicAccessLevels.TOKEN_BASED
# Anonymous users can have read-only access to token-based projects,
# while read-write access must be logged in,
# unless the `enableAnonymousReadAndWriteSharing` setting is enabled
TokenAccessHandler.isValidToken project_id, token, (err, isValidReadAndWrite, isValidReadOnly) ->
return callback(err) if err?
if isValidReadOnly
# Grant anonymous user read-only access
callback null, PrivilegeLevels.READ_ONLY, false, false
else if (
isValidReadAndWrite and
TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED
)
# Grant anonymous user read-and-write access
callback null, PrivilegeLevels.READ_AND_WRITE, false, false
else
# Deny anonymous access
callback null, PrivilegeLevels.NONE, false, false
else if publicAccessLevel == PublicAccessLevels.READ_ONLY
# Legacy public read-only access for anonymous user
callback null, PrivilegeLevels.READ_ONLY, true, false
else if publicAccessLevel == PublicAccessLevels.READ_AND_WRITE
# Legacy public read-write access for anonymous user
callback null, PrivilegeLevels.READ_AND_WRITE, true, false
else
# Deny anonymous user access
callback null, PrivilegeLevels.NONE, false, false
else
# User is present, get their privilege level from database
CollaboratorsHandler.getMemberIdPrivilegeLevel user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
if privilegeLevel? and privilegeLevel != PrivilegeLevels.NONE
# The user has direct access
callback null, privilegeLevel, false, false
else
AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) ->
return callback(error) if error?
if isAdmin
callback null, PrivilegeLevels.OWNER, false, true
else
# Legacy public-access system
# User is present (not anonymous), but does not have direct access
AuthorizationManager.getPublicAccessLevel project_id, (err, publicAccessLevel) ->
return callback(err) if err?
if publicAccessLevel == PublicAccessLevels.READ_ONLY
callback null, PrivilegeLevels.READ_ONLY, true, false
else if publicAccessLevel == PublicAccessLevels.READ_AND_WRITE
callback null, PrivilegeLevels.READ_AND_WRITE, true, false
else
callback null, PrivilegeLevels.NONE, false, false
canUserReadProject: (user_id, project_id, token, callback = (error, canRead) ->) ->
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) ->
return callback(error) if error?
return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.READ_ONLY])
canUserWriteProjectContent: (user_id, project_id, token, callback = (error, canWriteContent) ->) ->
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) ->
return callback(error) if error?
return callback null, (privilegeLevel in [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE])
canUserWriteProjectSettings: (user_id, project_id, token, callback = (error, canWriteSettings) ->) ->
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel, becausePublic) ->
return callback(error) if error?
if privilegeLevel == PrivilegeLevels.OWNER
return callback null, true
else if privilegeLevel == PrivilegeLevels.READ_AND_WRITE and !becausePublic
return callback null, true
else
return callback null, false
canUserAdminProject: (user_id, project_id, token, callback = (error, canAdmin, becauseSiteAdmin) ->) ->
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel, becausePublic, becauseSiteAdmin) ->
return callback(error) if error?
return callback null, (privilegeLevel == PrivilegeLevels.OWNER), becauseSiteAdmin
isUserSiteAdmin: (user_id, callback = (error, isAdmin) ->) ->
if !user_id?
return callback null, false
User.findOne { _id: user_id }, { isAdmin: 1 }, (error, user) ->
return callback(error) if error?
return callback null, (user?.isAdmin == true)

View File

@@ -1,122 +0,0 @@
AuthorizationManager = require("./AuthorizationManager")
async = require "async"
logger = require "logger-sharelatex"
ObjectId = require("mongojs").ObjectId
Errors = require "../Errors/Errors"
AuthenticationController = require "../Authentication/AuthenticationController"
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
module.exports = AuthorizationMiddleware =
ensureUserCanReadMultipleProjects: (req, res, next) ->
project_ids = (req.query.project_ids or "").split(",")
AuthorizationMiddleware._getUserId req, (error, user_id) ->
return next(error) if error?
# Remove the projects we have access to. Note rejectSeries doesn't use
# errors in callbacks
async.rejectSeries project_ids, (project_id, cb) ->
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.canUserReadProject user_id, project_id, token, (error, canRead) ->
return next(error) if error?
cb(canRead)
, (unauthorized_project_ids) ->
if unauthorized_project_ids.length > 0
AuthorizationMiddleware.redirectToRestricted req, res, next
else
next()
ensureUserCanReadProject: (req, res, next) ->
AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.canUserReadProject user_id, project_id, token, (error, canRead) ->
return next(error) if error?
if canRead
logger.log {user_id, project_id}, "allowing user read access to project"
next()
else
logger.log {user_id, project_id}, "denying user read access to project"
if req.headers?['accept']?.match(/^application\/json.*$/)
res.sendStatus(403)
else
AuthorizationMiddleware.redirectToRestricted req, res, next
ensureUserCanWriteProjectSettings: (req, res, next) ->
AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.canUserWriteProjectSettings user_id, project_id, token, (error, canWrite) ->
return next(error) if error?
if canWrite
logger.log {user_id, project_id}, "allowing user write access to project settings"
next()
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddleware.redirectToRestricted req, res, next
ensureUserCanWriteProjectContent: (req, res, next) ->
AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.canUserWriteProjectContent user_id, project_id, token, (error, canWrite) ->
return next(error) if error?
if canWrite
logger.log {user_id, project_id}, "allowing user write access to project content"
next()
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddleware.redirectToRestricted req, res, next
ensureUserCanAdminProject: (req, res, next) ->
AuthorizationMiddleware._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.canUserAdminProject user_id, project_id, token, (error, canAdmin) ->
return next(error) if error?
if canAdmin
logger.log {user_id, project_id}, "allowing user admin access to project"
next()
else
logger.log {user_id, project_id}, "denying user admin access to project"
AuthorizationMiddleware.redirectToRestricted req, res, next
ensureUserIsSiteAdmin: (req, res, next) ->
AuthorizationMiddleware._getUserId req, (error, user_id) ->
return next(error) if error?
AuthorizationManager.isUserSiteAdmin user_id, (error, isAdmin) ->
return next(error) if error?
if isAdmin
logger.log {user_id}, "allowing user admin access to site"
next()
else
logger.log {user_id}, "denying user admin access to site"
AuthorizationMiddleware.redirectToRestricted req, res, next
_getUserAndProjectId: (req, callback = (error, user_id, project_id) ->) ->
project_id = req.params?.project_id or req.params?.Project_id
if !project_id?
return callback(new Error("Expected project_id in request parameters"))
if !ObjectId.isValid(project_id)
return callback(new Errors.NotFoundError("invalid project_id: #{project_id}"))
AuthorizationMiddleware._getUserId req, (error, user_id) ->
return callback(error) if error?
callback(null, user_id, project_id)
_getUserId: (req, callback = (error, user_id) ->) ->
user_id = AuthenticationController.getLoggedInUserId(req) || req?.oauth_user?._id || null
return callback(null, user_id)
redirectToRestricted: (req, res, next) ->
# TODO: move this to throwing ForbiddenError
res.redirect "/restricted?from=#{encodeURIComponent(req.url)}"
restricted : (req, res, next)->
if AuthenticationController.isUserLoggedIn(req)
res.render 'user/restricted',
title:'restricted'
else
from = req.query.from
logger.log {from: from}, "redirecting to login"
redirect_to = "/login"
if from?
AuthenticationController.setRedirectInSession(req, from)
res.redirect redirect_to

View File

@@ -1,5 +0,0 @@
module.exports =
NONE: false
READ_ONLY: "readOnly"
READ_AND_WRITE: "readAndWrite"
OWNER: "owner"

View File

@@ -1,5 +0,0 @@
module.exports =
READ_ONLY: "readOnly" # LEGACY
READ_AND_WRITE: "readAndWrite" # LEGACY
PRIVATE: "private"
TOKEN_BASED: "tokenBased"

View File

@@ -1,4 +0,0 @@
module.exports =
INVITE: 'invite'
TOKEN: 'token'
OWNER: 'owner'

View File

@@ -1,40 +0,0 @@
BetaProgramHandler = require './BetaProgramHandler'
UserGetter = require "../User/UserGetter"
Settings = require "settings-sharelatex"
logger = require 'logger-sharelatex'
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = BetaProgramController =
optIn: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting in to beta program"
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optIn user_id, (err) ->
if err
return next(err)
return res.redirect "/beta/participate"
optOut: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting out of beta program"
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optOut user_id, (err) ->
if err
return next(err)
return res.redirect "/beta/participate"
optInPage: (req, res, next)->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "showing beta participation page for user"
UserGetter.getUser user_id, (err, user)->
if err
logger.err {err, user_id}, "error fetching user"
return next(err)
res.render 'beta_program/opt_in',
title:'sharelatex_beta_program'
user: user,
languages: Settings.languages,

View File

@@ -1,31 +0,0 @@
User = require("../../models/User").User
logger = require 'logger-sharelatex'
metrics = require("metrics-sharelatex")
module.exports = BetaProgramHandler =
optIn: (user_id, callback=(err)->) ->
User.findById user_id, (err, user) ->
if err
logger.err {err, user_id}, "problem adding user to beta"
return callback(err)
metrics.inc "beta-program.opt-in"
user.betaProgram = true
user.save (err) ->
if err
logger.err {err, user_id}, "problem adding user to beta"
return callback(err)
return callback(null)
optOut: (user_id, callback=(err)->) ->
User.findById user_id, (err, user) ->
if err
logger.err {err, user_id}, "problem removing user from beta"
return callback(err)
metrics.inc "beta-program.opt-out"
user.betaProgram = false
user.save (err) ->
if err
logger.err {err, user_id}, "problem removing user from beta"
return callback(err)
return callback(null)

View File

@@ -1,45 +0,0 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
_ = require("underscore")
ErrorController = require "../Errors/ErrorController"
module.exports = BlogController =
getPage: (req, res, next)->
url = req.url?.toLowerCase()
blogUrl = "#{settings.apis.blog.url}#{url}"
extensionsToProxy = [".png", ".xml", ".jpeg", ".jpg", ".json", ".zip", ".eps", ".gif"]
shouldProxy = _.find extensionsToProxy, (extension)->
url.indexOf(extension) != -1
if shouldProxy
return BlogController._directProxy blogUrl, res
logger.log url:url, "proxying request to blog api"
request.get blogUrl, (err, r, data)->
if r?.statusCode == 404 or r?.statusCode == 403
return ErrorController.notFound(req, res, next)
if err?
return res.send 500
data = data.trim()
try
data = JSON.parse(data)
if settings.cdn?.web?.host?
data?.content = data?.content?.replace(/src="(\/[^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
catch err
logger.err err:err, data:data, "error parsing data from data"
res.render "blog/blog_holder", data
getIndexPage: (req, res)->
req.url = "/blog/index.html"
BlogController.getPage req, res
_directProxy: (originUrl, res)->
upstream = request.get(originUrl)
upstream.on "error", (error) ->
logger.error err: error, "blog proxy error"
upstream.pipe res

View File

@@ -1,23 +0,0 @@
request = require "request"
settings = require "settings-sharelatex"
_ = require("underscore")
logger = require "logger-sharelatex"
module.exports = BlogHandler =
getLatestAnnouncements: (callback)->
blogUrl = "#{settings.apis.blog.url}/blog/latestannouncements.json"
opts =
url:blogUrl
json:true
timeout:1000
request.get opts, (err, res, announcements)->
if err?
return callback err
if res.statusCode != 200
return callback("blog announcement returned non 200")
logger.log announcementsLength: announcements?.length, "announcements returned"
announcements = _.map announcements, (announcement)->
announcement.date = new Date(announcement.date)
return announcement
callback(err, announcements)

View File

@@ -1,41 +0,0 @@
url = require "url"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
V1Api = require "../V1/V1Api"
module.exports = BrandVariationsHandler =
getBrandVariationById: (brandVariationId, callback = (error, brandVariationDetails) ->)->
if !brandVariationId? or brandVariationId == ""
return callback(new Error("Branding variation id not provided"))
logger.log brandVariationId: brandVariationId, "fetching brand variation details from v1"
V1Api.request {
uri: "/api/v2/brand_variations/#{brandVariationId}"
}, (error, response, brandVariationDetails) ->
if error?
logger.err { brandVariationId, error}, "error getting brand variation details"
return callback(error)
_formatBrandVariationDetails brandVariationDetails
callback(null, brandVariationDetails)
_formatBrandVariationDetails = (details) ->
if details.export_url?
details.export_url = _setV1AsHostIfRelativeURL details.export_url
if details.home_url?
details.home_url = _setV1AsHostIfRelativeURL details.home_url
if details.logo_url?
details.logo_url = _setV1AsHostIfRelativeURL details.logo_url
if details.journal_guidelines_url?
details.journal_guidelines_url = _setV1AsHostIfRelativeURL details.journal_guidelines_url
if details.journal_cover_url?
details.journal_cover_url = _setV1AsHostIfRelativeURL details.journal_cover_url
if details.submission_confirmation_page_logo_url?
details.submission_confirmation_page_logo_url = _setV1AsHostIfRelativeURL details.submission_confirmation_page_logo_url
if details.publish_menu_icon?
details.publish_menu_icon = _setV1AsHostIfRelativeURL details.publish_menu_icon
_setV1AsHostIfRelativeURL = (urlString) ->
# The first argument is the base URL to resolve against if the second argument is not absolute.
# As it only applies if the second argument is not absolute, we can use it to transform relative URLs into
# absolute ones using v1 as the host. If the URL is absolute (e.g. a filepicker one), then the base
# argument is just ignored
url.resolve settings?.apis?.v1?.url, urlString

View File

@@ -1,28 +0,0 @@
request = require 'request'
logger = require 'logger-sharelatex'
Settings = require 'settings-sharelatex'
module.exports = CaptchaMiddleware =
validateCaptcha: (action) ->
return (req, res, next) ->
if !Settings.recaptcha?.siteKey?
return next()
inviteAndCaptchaDisabled = action == 'invite' and Settings.recaptcha.disabled.invite
registerAndCaptchaDisabled = action == 'register' and Settings.recaptcha.disabled.register
if inviteAndCaptchaDisabled or registerAndCaptchaDisabled
return next()
response = req.body['g-recaptcha-response']
options =
form:
secret: Settings.recaptcha.secretKey
response: response
json: true
request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) ->
return next(error) if error?
if !body?.success
logger.warn {statusCode: response.statusCode, body: body}, 'failed recaptcha siteverify request'
return res.status(400).send({errorReason:"cannot_verify_user_not_robot", message:
{text:"Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall."}
})
else
return next()

View File

@@ -1,82 +0,0 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports = ChatApiHandler =
_apiRequest: (opts, callback = (error, data) ->) ->
request opts, (error, response, data) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
return callback null, data
else
error = new Error("chat api returned non-success code: #{response.statusCode}")
error.statusCode = response.statusCode
logger.error {err: error, opts}, "error sending request to chat api"
return callback error
sendGlobalMessage: (project_id, user_id, content, callback)->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
method: "POST"
json: {user_id, content}
}, callback
getGlobalMessages: (project_id, limit, before, callback)->
qs = {}
qs.limit = limit if limit?
qs.before = before if before?
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages"
method: "GET"
qs: qs
json: true
}, callback
sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages"
method: "POST"
json: {user_id, content}
}, callback
getThreads: (project_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads"
method: "GET"
json: true
}, callback
resolveThread: (project_id, thread_id, user_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve"
method: "POST"
json: {user_id}
}, callback
reopenThread: (project_id, thread_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen"
method: "POST"
}, callback
deleteThread: (project_id, thread_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}"
method: "DELETE"
}, callback
editMessage: (project_id, thread_id, message_id, content, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}/edit"
method: "POST"
json:
content: content
}, callback
deleteMessage: (project_id, thread_id, message_id, callback = (error) ->) ->
ChatApiHandler._apiRequest {
url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages/#{message_id}"
method: "DELETE"
}, callback

View File

@@ -1,65 +0,0 @@
ChatApiHandler = require("./ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
async = require "async"
module.exports = ChatController =
sendMessage: (req, res, next)->
project_id = req.params.project_id
content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
return next(err) if err?
UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
return next(err) if err?
message.user = UserInfoController.formatPersonalInfo(user)
EditorRealTimeController.emitToRoom project_id, "new-chat-message", message
res.send(204)
getMessages: (req, res, next)->
project_id = req.params.project_id
query = req.query
logger.log project_id:project_id, query:query, "getting messages"
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
return next(err) if err?
ChatController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
return next(err) if err?
logger.log length: messages?.length, "sending messages to client"
res.json messages
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
# There will be a lot of repitition of user_ids, so first build a list
# of unique ones to perform db look ups on, then use these to populate the
# user fields
user_ids = {}
for thread_id, thread of threads
if thread.resolved
user_ids[thread.resolved_by_user_id] = true
for message in thread.messages
user_ids[message.user_id] = true
jobs = []
users = {}
for user_id, _ of user_ids
do (user_id) ->
jobs.push (cb) ->
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return cb(error) if error?
user = UserInfoController.formatPersonalInfo user
users[user_id] = user
cb()
async.series jobs, (error) ->
return callback(error) if error?
for thread_id, thread of threads
if thread.resolved
thread.resolved_by_user = users[thread.resolved_by_user_id]
for message in thread.messages
message.user = users[message.user_id]
return callback null, threads

View File

@@ -1,40 +0,0 @@
ProjectGetter = require "../Project/ProjectGetter"
CollaboratorsHandler = require "./CollaboratorsHandler"
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
EmailHelper = require "../Helpers/EmailHelper"
logger = require 'logger-sharelatex'
module.exports = CollaboratorsController =
removeUserFromProject: (req, res, next) ->
project_id = req.params.Project_id
user_id = req.params.user_id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true}
res.sendStatus 204
removeSelfFromProject: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = req.session?.user?._id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
_removeUserIdFromProject: (project_id, user_id, callback = (error) ->) ->
CollaboratorsHandler.removeUserFromProject project_id, user_id, (error)->
return callback(error) if error?
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
getAllMembers: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active members for project"
CollaboratorsHandler.getAllInvitedMembers projectId, (err, members) ->
if err?
logger.err {projectId}, "error getting members for project"
return next(err)
res.json({members: members})

View File

@@ -1,28 +0,0 @@
Project = require("../../models/Project").Project
EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
module.exports = CollaboratorsEmailHandler =
_buildInviteUrl: (project, invite) ->
"#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
to: email
replyTo: project.owner_ref.email
project:
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
sendingUser_id: sendingUser._id
EmailHandler.sendEmail "projectInvite", emailOptions, callback

View File

@@ -1,307 +0,0 @@
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
ProjectGetter = require('../Project/ProjectGetter')
logger = require('logger-sharelatex')
UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
PublicAccessLevels = require "../Authorization/PublicAccessLevels"
Errors = require "../Errors/Errors"
EmailHelper = require "../Helpers/EmailHelper"
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
Sources = require "../Authorization/Sources"
ObjectId = require('mongojs').ObjectId
module.exports = CollaboratorsHandler =
getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
projection =
owner_ref: 1,
collaberator_refs: 1,
readOnly_refs: 1,
tokenAccessReadOnly_refs: 1,
tokenAccessReadAndWrite_refs: 1
publicAccesLevel: 1
ProjectGetter.getProject project_id, projection, (error, project) ->
return callback(error) if error?
return callback new Errors.NotFoundError("no project found with id #{project_id}") if !project?
members = []
members.push {
id: project.owner_ref.toString(),
privilegeLevel: PrivilegeLevels.OWNER,
source: Sources.OWNER
}
for member_id in project.collaberator_refs or []
members.push {
id: member_id.toString(),
privilegeLevel: PrivilegeLevels.READ_AND_WRITE,
source: Sources.INVITE
}
for member_id in project.readOnly_refs or []
members.push {
id: member_id.toString(),
privilegeLevel: PrivilegeLevels.READ_ONLY,
source: Sources.INVITE
}
if project.publicAccesLevel == PublicAccessLevels.TOKEN_BASED
for member_id in project.tokenAccessReadAndWrite_refs or []
members.push {
id: member_id.toString(),
privilegeLevel: PrivilegeLevels.READ_AND_WRITE,
source: Sources.TOKEN
}
for member_id in project.tokenAccessReadOnly_refs or []
members.push {
id: member_id.toString(),
privilegeLevel: PrivilegeLevels.READ_ONLY,
source: Sources.TOKEN
}
return callback null, members
getMemberIds: (project_id, callback = (error, member_ids) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, members.map (m) -> m.id
getInvitedMemberIds: (project_id, callback = (error, member_ids) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, members.filter((m) -> m.source != Sources.TOKEN).map((m) -> m.id)
USER_PROJECTION: {
_id: 1,
email: 1,
features: 1,
first_name: 1,
last_name: 1,
signUpDate: 1
}
_loadMembers: (members, callback=(error, users) ->) ->
result = []
async.mapLimit members, 3,
(member, cb) ->
UserGetter.getUserOrUserStubById member.id, CollaboratorsHandler.USER_PROJECTION, (error, user) ->
return cb(error) if error?
if user?
result.push { user: user, privilegeLevel: member.privilegeLevel }
cb()
(error) ->
return callback(error) if error?
callback null, result
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
CollaboratorsHandler._loadMembers members, (error, users) ->
callback error, users
getInvitedMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
members = members.filter((m) -> m.source != Sources.TOKEN)
CollaboratorsHandler._loadMembers members, (error, users) ->
callback error, users
getTokenMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
members = members.filter((m) -> m.source == Sources.TOKEN)
CollaboratorsHandler._loadMembers members, (error, users) ->
callback error, users
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
# then optimise this.
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
for member in members
if member.id == user_id?.toString()
return callback null, member.privilegeLevel
return callback null, PrivilegeLevels.NONE
getInvitedMemberCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, (members or []).filter((m) -> m.source != Sources.TOKEN).length
getInvitedCollaboratorCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getInvitedMemberCount project_id, (error, count) ->
return callback(error) if error?
return callback null, count - 1 # Don't count project owner
isUserInvitedMemberOfProject: (user_id, project_id, callback = (error, isMember, privilegeLevel) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
for member in members
if member.id.toString() == user_id.toString() and member.source != Sources.TOKEN
return callback null, true, member.privilegeLevel
return callback null, false, null
getProjectsUserIsMemberOf: (user_id, fields, callback = (error, {readAndWrite:[],readOnly:[],tokenReadAndWrite:[],tokenReadOnly:[]}) ->) ->
async.mapLimit(
[
{collaberator_refs: user_id},
{readOnly_refs: user_id},
{
tokenAccessReadAndWrite_refs: user_id,
publicAccesLevel: PublicAccessLevels.TOKEN_BASED
},
{
tokenAccessReadOnly_refs: user_id,
publicAccesLevel: PublicAccessLevels.TOKEN_BASED
}
]
, 2
, (query, cb) ->
Project.find query, fields, cb
, (error, results) ->
return callback(error) if error?
projects = {
readAndWrite: results[0]
readOnly: results[1]
tokenReadAndWrite: results[2]
tokenReadOnly: results[3]
}
callback(null, projects)
)
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
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
tokenAccessReadOnly_refs:user_id
tokenAccessReadAndWrite_refs:user_id
Project.update conditions, update, (err)->
if err?
logger.error err: err, "problem removing user from project collaberators"
callback(err)
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
CollaboratorsHandler.getProjectsUserIsMemberOf user_id, { _id: 1 }, (error, {readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly}) ->
return callback(error) if error?
allProjects =
readAndWrite
.concat(readOnly)
.concat(tokenReadAndWrite)
.concat(tokenReadOnly)
jobs = []
for project in allProjects
do (project) ->
jobs.push (cb) ->
return cb() if !project?
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
async.series jobs, callback
addUserIdToProject: (project_id, adding_user_id, user_id, privilegeLevel, callback = (error) ->)->
ProjectGetter.getProject project_id, { collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
return callback(error) if error?
existing_users = (project.collaberator_refs or [])
existing_users = existing_users.concat(project.readOnly_refs or [])
existing_users = existing_users.map (u) -> u.toString()
if existing_users.indexOf(user_id.toString()) > -1
return callback null # User already in Project
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
level = {"collaberator_refs":user_id}
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
else if privilegeLevel == PrivilegeLevels.READ_ONLY
level = {"readOnly_refs":user_id}
logger.log {privileges: "readOnly", user_id, project_id}, "adding user"
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
if adding_user_id
ContactManager.addContact adding_user_id, user_id
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
return callback(error) if error?
# Flush to TPDS in background to add files to collaborator's Dropbox
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, (error) ->
if error?
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
callback()
getAllInvitedMembers: (projectId, callback=(err, members)->) ->
logger.log {projectId}, "fetching all members"
CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels projectId, (error, rawMembers) ->
if error?
logger.err {projectId, error}, "error getting members for project"
return callback(error)
{owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers)
callback(null, members)
userIsTokenMember: (userId, projectId, callback=(err, isTokenMember)->) ->
userId = ObjectId(userId.toString())
projectId = ObjectId(projectId.toString())
Project.findOne {
_id: projectId,
$or: [
{tokenAccessReadOnly_refs: userId},
{tokenAccessReadAndWrite_refs: userId}
]
}, {
_id: 1
}, (err, project) ->
if err?
return callback(err)
callback(null, project?)
transferProjects: (from_user_id, to_user_id, callback=(err, projects) ->) ->
MEMBER_KEYS = ['collaberator_refs', 'readOnly_refs']
# Find all the projects this user is part of so we can flush them to TPDS
query =
$or:
[{ owner_ref: from_user_id }]
.concat(
MEMBER_KEYS.map (key) ->
q = {}
q[key] = from_user_id
return q
) # [{ collaberator_refs: from_user_id }, ...]
Project.find query, { _id: 1 }, (error, projects = []) ->
return callback(error) if error?
project_ids = projects.map (p) -> p._id
logger.log {project_ids, from_user_id, to_user_id}, "transferring projects"
update_jobs = []
update_jobs.push (cb) ->
Project.update { owner_ref: from_user_id }, { $set: { owner_ref: to_user_id }}, { multi: true }, cb
for key in MEMBER_KEYS
do (key) ->
update_jobs.push (cb) ->
query = {}
addNewUserUpdate = $addToSet: {}
removeOldUserUpdate = $pull: {}
query[key] = from_user_id
removeOldUserUpdate.$pull[key] = from_user_id
addNewUserUpdate.$addToSet[key] = to_user_id
# Mongo won't let us pull and addToSet in the same query, so do it in
# two. Note we need to add first, since the query is based on the old user.
Project.update query, addNewUserUpdate, { multi: true }, (error) ->
return cb(error) if error?
Project.update query, removeOldUserUpdate, { multi: true }, cb
# Flush each project to TPDS to add files to new user's Dropbox
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
flush_jobs = []
for project_id in project_ids
do (project_id) ->
flush_jobs.push (cb) ->
ProjectEntityHandler.flushProjectToThirdPartyDataStore project_id, cb
# Flush in background, no need to block on this
async.series flush_jobs, (error) ->
if error?
logger.err {err: error, project_ids, from_user_id, to_user_id}, "error flushing tranferred projects to TPDS"
async.series update_jobs, (err) ->
logger.log("flushed transferred projects to TPDS")
callback(err)

View File

@@ -1,171 +0,0 @@
ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
AnalyticsManger = require("../Analytics/AnalyticsManager")
AuthenticationController = require("../Authentication/AuthenticationController")
rateLimiter = require("../../infrastructure/RateLimiter")
request = require 'request'
module.exports = CollaboratorsInviteController =
getAllInvites: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active invites for project"
CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) ->
if err?
logger.err {projectId}, "error getting invites for project"
return next(err)
res.json({invites: invites})
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
if Settings.restrictInvitesToExistingAccounts == true
logger.log {email}, "checking if user exists with this email"
UserGetter.getUserByAnyEmail email, {_id: 1}, (err, user) ->
return callback(err) if err?
userExists = user? and user?._id?
callback(null, userExists)
else
callback(null, true)
_checkRateLimit: (user_id, callback = (error) ->) ->
LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
return callback(err) if err?
if collabLimit == -1
collabLimit = 20
collabLimit = collabLimit * 10
opts =
endpointName: "invite-to-project-by-user-id"
timeInterval: 60 * 30
subjectName: user_id
throttle: collabLimit
rateLimiter.addCount opts, callback
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
sendingUser = AuthenticationController.getSessionUser(req)
sendingUserId = sendingUser._id
if email == sendingUser.email
logger.log {projectId, email, sendingUserId}, "cannot invite yourself to project"
return res.json {invite: null, error: 'cannot_invite_self'}
logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
if !allowed
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {invite: null}
{email, privileges} = req.body
email = EmailHelper.parseEmail(email)
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.status(400).send({errorReason:"invalid_email"})
CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
return next(error) if error?
if !underRateLimit
return res.sendStatus(429)
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)->
if err?
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
return next(err)
if !shouldAllowInvite
logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
return res.json {invite: null, error: 'cannot_invite_non_user'}
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
if err?
logger.err {projectId, email, sendingUserId}, "error creating project invite"
return next(err)
logger.log {projectId, email, sendingUserId}, "invite created"
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
return res.json {invite: invite}
revokeInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "revoking invite"
CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error revoking invite"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
res.sendStatus(201)
resendInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "resending invite"
sendingUser = AuthenticationController.getSessionUser(req)
CollaboratorsInviteController._checkRateLimit sendingUser._id, (error, underRateLimit) ->
return next(error) if error?
if !underRateLimit
return res.sendStatus(429)
CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite"
return next(err)
res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
# check if the user is already a member of the project
currentUser = AuthenticationController.getSessionUser(req)
CollaboratorsHandler.isUserInvitedMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
logger.err {err, projectId}, "error checking if user is member of project"
return next(err)
if isMember
logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
return res.redirect "/project/#{projectId}"
# get the invite
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
# check if invite is gone, or otherwise non-existent
if !invite?
logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
# check the user who sent the invite exists
UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
if !owner?
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
# fetch the project name
ProjectGetter.getProject projectId, {}, (err, project) ->
if err?
logger.err {err, projectId}, "error getting project"
return next(err)
if !project?
logger.log {projectId}, "no project found"
return _renderInvalidPage()
# finally render the invite
res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
currentUser = AuthenticationController.getSessionUser(req)
logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite"
CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) ->
if err?
logger.err {projectId, token}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id})
if req.xhr
res.sendStatus 204 # Done async via project page notification
else
res.redirect "/project/#{projectId}"

View File

@@ -1,144 +0,0 @@
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
logger = require('logger-sharelatex')
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
CollaboratorsHandler = require "./CollaboratorsHandler"
UserGetter = require "../User/UserGetter"
ProjectGetter = require "../Project/ProjectGetter"
Async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
Crypto = require 'crypto'
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = CollaboratorsInviteHandler =
getAllInvites: (projectId, callback=(err, invites)->) ->
logger.log {projectId}, "fetching invites for project"
ProjectInvite.find {projectId: projectId}, (err, invites) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
logger.log {projectId, count: invites.length}, "found invites for project"
callback(null, invites)
getInviteCount: (projectId, callback=(err, count)->) ->
logger.log {projectId}, "counting invites for project"
ProjectInvite.count {projectId: projectId}, (err, count) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
callback(null, count)
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
email = invite.email
UserGetter.getUserByAnyEmail email, {_id: 1}, (err, existingUser) ->
if err?
logger.err {projectId, email}, "error checking if user exists"
return callback(err)
if !existingUser?
logger.log {projectId, email}, "no existing user found, returning"
return callback(null)
ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
if err?
logger.err {projectId, email}, "error getting project"
return callback(err)
if !project?
logger.log {projectId}, "no project found while sending notification, returning"
return callback(null)
NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
_tryCancelInviteNotification: (inviteId, callback=()->) ->
NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback)
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
return callback(err) if err?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err?
callback()
inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) ->
logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite"
Crypto.randomBytes 24, (err, buffer) ->
if err?
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token"
return callback(err)
token = buffer.toString('hex')
invite = new ProjectInvite {
email: email
token: token
sendingUserId: sendingUser._id
projectId: projectId
privileges: privileges
}
invite.save (err, invite) ->
if err?
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
return callback(err)
# Send email and notification in background
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
logger.err {err, projectId, email}, "error sending messages for invite"
callback(null, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "removing invite"
ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
callback(null)
resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "resending invite email"
ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
if !invite?
logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
return callback(null)
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite messages"
return callback(err)
callback(null)
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
logger.log {projectId, tokenString}, "fetching invite by token"
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
if err?
logger.err {err, projectId}, "error fetching invite"
return callback(err)
if !invite?
logger.err {err, projectId, token: tokenString}, "no invite found"
return callback(null, null)
callback(null, invite)
acceptInvite: (projectId, tokenString, user, callback=(err)->) ->
logger.log {projectId, userId: user._id, tokenString}, "accepting invite"
CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
if err?
logger.err {err, projectId, tokenString}, "error finding invite"
return callback(err)
if !invite
err = new Errors.NotFoundError("no matching invite found")
logger.log {err, projectId, tokenString}, "no matching invite found"
return callback(err)
inviteId = invite._id
CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
if err?
logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
return callback(err)
# Remove invite
logger.log {projectId, inviteId}, "removing invite"
ProjectInvite.remove {_id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
callback()

View File

@@ -1,79 +0,0 @@
CollaboratorsController = require('./CollaboratorsController')
AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
CollaboratorsInviteController = require('./CollaboratorsInviteController')
RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
CaptchaMiddleware = require '../Captcha/CaptchaMiddleware'
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.post '/project/:Project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddleware.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
webRouter.get(
'/project/:Project_id/members',
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsController.getAllMembers
)
# invites
webRouter.post(
'/project/:Project_id/invite',
RateLimiterMiddleware.rateLimit({
endpointName: "invite-to-project-by-project-id"
params: ["Project_id"]
maxRequests: 100
timeInterval: 60 * 10
}),
RateLimiterMiddleware.rateLimit({
endpointName: "invite-to-project-by-ip"
ipOnly:true
maxRequests: 100
timeInterval: 60 * 10
}),
CaptchaMiddleware.validateCaptcha('invite'),
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsInviteController.inviteToProject
)
webRouter.get(
'/project/:Project_id/invites',
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsInviteController.getAllInvites
)
webRouter.delete(
'/project/:Project_id/invite/:invite_id',
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsInviteController.revokeInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/resend',
RateLimiterMiddleware.rateLimit({
endpointName: "resend-invite"
params: ["Project_id"]
maxRequests: 200
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanAdminProject,
CollaboratorsInviteController.resendInvite
)
webRouter.get(
'/project/:Project_id/invite/token/:token',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.viewInvite
)
webRouter.post(
'/project/:Project_id/invite/token/:token/accept',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite
)

View File

@@ -1,81 +0,0 @@
Settings = require "settings-sharelatex"
request = require('request')
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("clsi_cookie")
if Settings.redis.clsi_cookie_secondary?
rclient_secondary = RedisWrapper.client("clsi_cookie_secondary")
Cookie = require('cookie')
logger = require "logger-sharelatex"
clsiCookiesEnabled = Settings.clsiCookie?.key? and Settings.clsiCookie.key.length != 0
module.exports = (backendGroup)->
buildKey : (project_id)->
if backendGroup?
return "clsiserver:#{backendGroup}:#{project_id}"
else
return "clsiserver:#{project_id}"
_getServerId : (project_id, callback = (err, serverId)->)->
rclient.get @buildKey(project_id), (err, serverId)=>
if err?
return callback(err)
if !serverId? or serverId == ""
return @_populateServerIdViaRequest project_id, callback
else
return callback(null, serverId)
_populateServerIdViaRequest :(project_id, callback = (err, serverId)->)->
url = "#{Settings.apis.clsi.url}/project/#{project_id}/status"
request.get url, (err, res, body)=>
if err?
logger.err err:err, project_id:project_id, "error getting initial server id for project"
return callback(err)
@setServerId project_id, res, (err, serverId)->
if err?
logger.err err:err, project_id:project_id, "error setting server id via populate request"
callback(err, serverId)
_parseServerIdFromResponse : (response)->
cookies = Cookie.parse(response.headers["set-cookie"]?[0] or "")
return cookies?[Settings.clsiCookie.key]
setServerId: (project_id, response, callback = (err, serverId)->)->
if !clsiCookiesEnabled
return callback()
serverId = @_parseServerIdFromResponse(response)
if !serverId? # We don't get a cookie back if it hasn't changed
return rclient.expire(@buildKey(project_id), Settings.clsiCookie.ttl, callback)
if rclient_secondary?
@_setServerIdInRedis rclient_secondary, project_id, serverId
@_setServerIdInRedis rclient, project_id, serverId, (err) ->
callback(err, serverId)
_setServerIdInRedis: (rclient, project_id, serverId, callback = (err) ->) ->
multi = rclient.multi()
multi.set @buildKey(project_id), serverId
multi.expire @buildKey(project_id), Settings.clsiCookie.ttl
multi.exec callback
clearServerId: (project_id, callback = (err)->)->
if !clsiCookiesEnabled
return callback()
rclient.del @buildKey(project_id), callback
getCookieJar: (project_id, callback = (err, jar)->)->
if !clsiCookiesEnabled
return callback(null, request.jar())
@_getServerId project_id, (err, serverId)=>
if err?
logger.err err:err, project_id:project_id, "error getting server id"
return callback(err)
serverCookie = request.cookie("#{Settings.clsiCookie.key}=#{serverId}")
jar = request.jar()
jar.setCookie serverCookie, Settings.apis.clsi.url
callback(null, jar)

View File

@@ -1,69 +0,0 @@
_ = require("lodash")
async = require("async")
settings = require("settings-sharelatex")
module.exports = ClsiFormatChecker =
checkRecoursesForProblems: (resources, callback)->
jobs =
conflictedPaths: (cb)->
ClsiFormatChecker._checkForConflictingPaths resources, cb
sizeCheck: (cb)->
ClsiFormatChecker._checkDocsAreUnderSizeLimit resources, cb
async.series jobs, (err, problems)->
if err?
return callback(err)
problems = _.omitBy(problems, _.isEmpty)
if _.isEmpty(problems)
return callback()
else
callback(null, problems)
_checkForConflictingPaths: (resources, callback)->
paths = _.map(resources, 'path')
conflicts = _.filter paths, (path)->
matchingPaths = _.filter paths, (checkPath)->
return checkPath.indexOf(path+"/") != -1
return matchingPaths.length > 0
conflictObjects = _.map conflicts, (conflict)->
path:conflict
callback null, conflictObjects
_checkDocsAreUnderSizeLimit: (resources, callback)->
sizeLimit = 1000 * 1000 * settings.compileBodySizeLimitMb
totalSize = 0
sizedResources = _.map resources, (resource)->
result = {path:resource.path}
if resource.content?
result.size = resource.content.replace(/\n/g).length
result.kbSize = Math.ceil(result.size / 1000)
else
result.size = 0
totalSize += result.size
return result
tooLarge = totalSize > sizeLimit
if !tooLarge
return callback()
else
sizedResources = _.sortBy(sizedResources, "size").reverse().slice(0, 10)
return callback(null, {resources:sizedResources, totalSize:totalSize})

View File

@@ -1,344 +0,0 @@
Path = require "path"
async = require "async"
Settings = require "settings-sharelatex"
request = require('request')
Project = require("../../models/Project").Project
ProjectGetter = require("../Project/ProjectGetter")
ProjectEntityHandler = require("../Project/ProjectEntityHandler")
logger = require "logger-sharelatex"
Url = require("url")
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
NewBackendCloudClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi_new?.backendGroupName)
ClsiStateManager = require("./ClsiStateManager")
_ = require("underscore")
async = require("async")
ClsiFormatChecker = require("./ClsiFormatChecker")
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
Metrics = require('metrics-sharelatex')
Errors = require ('../Errors/Errors')
module.exports = ClsiManager =
sendRequest: (project_id, user_id, options = {}, callback) ->
ClsiManager.sendRequestOnce project_id, user_id, options, (error, status, result...) ->
return callback(error) if error?
if status is 'conflict'
options = _.clone(options)
options.syncType = "full" # force full compile
ClsiManager.sendRequestOnce project_id, user_id, options, callback # try again
else
callback(error, status, result...)
sendRequestOnce: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
ClsiManager._buildRequest project_id, options, (error, req) ->
if error?
if error.message is "no main file specified"
return callback(null, "validation-problems", null, null, {mainFile:error.message})
else
return callback(error)
logger.log project_id: project_id, "sending compile to CLSI"
ClsiManager._sendBuiltRequest project_id, user_id, req, options, (error, status, result...) ->
return callback(error) if error?
callback(error, status, result...)
# for public API requests where there is no project id
sendExternalRequest: (submission_id, clsi_request, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
logger.log submission_id: submission_id, "sending external compile to CLSI", clsi_request
ClsiManager._sendBuiltRequest submission_id, null, clsi_request, options, (error, status, result...) ->
return callback(error) if error?
callback(error, status, result...)
stopCompile: (project_id, user_id, options, callback = (error) ->) ->
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")
opts =
url:compilerUrl
method:"POST"
ClsiManager._makeRequest project_id, opts, callback
deleteAuxFiles: (project_id, user_id, options, callback = (error) ->) ->
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id)
opts =
url:compilerUrl
method:"DELETE"
ClsiManager._makeRequest project_id, opts, (clsiError) ->
# always clear the project state from the docupdater, even if there
# was a problem with the request to the clsi
DocumentUpdaterHandler.clearProjectState project_id, (docUpdaterError) ->
error = clsiError or docUpdaterError
return callback(error) if error?
callback()
_sendBuiltRequest: (project_id, user_id, req, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
if err?
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
return callback(err)
if validationProblems?
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
return callback(null, "validation-problems", null, null, validationProblems)
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
if error?
logger.err err:error, project_id:project_id, "error sending request to clsi"
return callback(error)
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
if err?
logger.err err:err, project_id:project_id, "error getting server id"
return callback(err)
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
callback(null, response?.compile?.status, outputFiles, clsiServerId)
_makeRequest: (project_id, opts, callback)->
async.series {
currentBackend: (cb)->
startTime = new Date()
ClsiCookieManager.getCookieJar project_id, (err, jar)->
if err?
logger.err err:err, "error getting cookie jar for clsi request"
return callback(err)
opts.jar = jar
timer = new Metrics.Timer("compile.currentBackend")
request opts, (err, response, body)->
timer.done()
Metrics.inc "compile.currentBackend.response.#{response?.statusCode}"
if err?
logger.err err:err, project_id:project_id, url:opts?.url, "error making request to clsi"
return callback(err)
ClsiCookieManager.setServerId project_id, response, (err)->
if err?
logger.warn err:err, project_id:project_id, "error setting server id"
callback err, response, body #return as soon as the standard compile has returned
cb(err, {response:response, body:body, finishTime:new Date() - startTime })
newBackend: (cb)->
startTime = new Date()
ClsiManager._makeNewBackendRequest project_id, opts, (err, response, body)->
Metrics.inc "compile.newBackend.response.#{response?.statusCode}"
cb(err, {response:response, body:body, finishTime:new Date() - startTime})
}, (err, results)->
timeDifference = results.newBackend?.finishTime - results.currentBackend?.finishTime
statusCodeSame = results.newBackend?.response?.statusCode == results.currentBackend?.response?.statusCode
currentCompileTime = results.currentBackend?.finishTime
newBackendCompileTime = results.newBackend?.finishTime
logger.log {statusCodeSame, timeDifference, currentCompileTime, newBackendCompileTime, project_id}, "both clsi requests returned"
_makeNewBackendRequest: (project_id, baseOpts, callback)->
if !Settings.apis.clsi_new?.url?
return callback()
opts = _.clone(baseOpts)
opts.url = opts.url.replace(Settings.apis.clsi.url, Settings.apis.clsi_new?.url)
NewBackendCloudClsiCookieManager.getCookieJar project_id, (err, jar)->
if err?
logger.err err:err, "error getting cookie jar for clsi request"
return callback(err)
opts.jar = jar
timer = new Metrics.Timer("compile.newBackend")
request opts, (err, response, body)->
timer.done()
if err?
logger.warn err:err, project_id:project_id, url:opts?.url, "error making request to new clsi"
return callback(err)
NewBackendCloudClsiCookieManager.setServerId project_id, response, (err)->
if err?
logger.warn err:err, project_id:project_id, "error setting server id new backend"
return callback err, response, body
_getCompilerUrl: (compileGroup, project_id, user_id, action) ->
host = Settings.apis.clsi.url
path = "/project/#{project_id}"
path += "/user/#{user_id}" if user_id?
path += "/#{action}" if action?
return "#{host}#{path}"
_postToClsi: (project_id, user_id, req, compileGroup, callback = (error, response) ->) ->
compileUrl = @_getCompilerUrl(compileGroup, project_id, user_id, "compile")
opts =
url: compileUrl
json: req
method: "POST"
ClsiManager._makeRequest project_id, opts, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
callback null, body
else if response.statusCode == 413
callback null, compile:status:"project-too-large"
else if response.statusCode == 409
callback null, compile:status:"conflict"
else if response.statusCode == 423
callback null, compile:status:"compile-in-progress"
else
error = new Error("CLSI returned non-success code: #{response.statusCode}")
logger.error err: error, project_id: project_id, "CLSI returned failure code"
callback error, body
_parseOutputFiles: (project_id, rawOutputFiles = []) ->
outputFiles = []
for file in rawOutputFiles
outputFiles.push
path: file.path # the clsi is now sending this to web
url: Url.parse(file.url).path # the location of the file on the clsi, excluding the host part
type: file.type
build: file.build
return outputFiles
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
_buildRequest: (project_id, options={}, callback = (error, request) ->) ->
ProjectGetter.getProject project_id, {compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder:1}, (error, project) ->
return callback(error) if error?
return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
if project.compiler not in ClsiManager.VALID_COMPILERS
project.compiler = "pdflatex"
if options.incrementalCompilesEnabled or options.syncType? # new way, either incremental or full
timer = new Metrics.Timer("editor.compile-getdocs-redis")
ClsiManager.getContentFromDocUpdaterIfMatch project_id, project, options, (error, projectStateHash, docUpdaterDocs) ->
timer.done()
if error?
logger.error err: error, project_id: project_id, "error checking project state"
# note: we don't bail out when there's an error getting
# incremental files from the docupdater, we just fall back
# to a normal compile below
else
logger.log project_id: project_id, projectStateHash: projectStateHash, docs: docUpdaterDocs?, "checked project state"
# see if we can send an incremental update to the CLSI
if docUpdaterDocs? and (options.syncType isnt "full") and not error?
Metrics.inc "compile-from-redis"
ClsiManager._buildRequestFromDocupdater project_id, options, project, projectStateHash, docUpdaterDocs, callback
else
Metrics.inc "compile-from-mongo"
ClsiManager._buildRequestFromMongo project_id, options, project, projectStateHash, callback
else # old way, always from mongo
timer = new Metrics.Timer("editor.compile-getdocs-mongo")
ClsiManager._getContentFromMongo project_id, (error, docs, files) ->
timer.done()
return callback(error) if error?
ClsiManager._finaliseRequest project_id, options, project, docs, files, callback
getContentFromDocUpdaterIfMatch: (project_id, project, options, callback = (error, projectStateHash, docs) ->) ->
ClsiStateManager.computeHash project, options, (error, projectStateHash) ->
return callback(error) if error?
DocumentUpdaterHandler.getProjectDocsIfMatch project_id, projectStateHash, (error, docs) ->
return callback(error) if error?
callback(null, projectStateHash, docs)
getOutputFileStream: (project_id, user_id, build_id, output_file_path, callback=(err, readStream)->) ->
url = "#{Settings.apis.clsi.url}/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{output_file_path}"
ClsiCookieManager.getCookieJar project_id, (err, jar)->
return callback(err) if err?
options = { url: url, method: "GET", timeout: 60 * 1000, jar : jar }
readStream = request(options)
callback(null, readStream)
_buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) ->
ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) ->
return callback(error) if error?
docs = {}
for doc in docUpdaterDocs or []
path = docPath[doc._id]
docs[path] = doc
# send new docs but not files as those are already on the clsi
options = _.clone(options)
options.syncType = "incremental"
options.syncState = projectStateHash
# create stub doc entries for any possible root docs, if not
# present in the docupdater. This allows finaliseRequest to
# identify the root doc.
possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id]
for rootDoc_id in possibleRootDocIds when rootDoc_id? and rootDoc_id of docPath
path = docPath[rootDoc_id]
docs[path] ?= {_id: rootDoc_id, path: path}
ClsiManager._finaliseRequest project_id, options, project, docs, [], callback
_buildRequestFromMongo: (project_id, options, project, projectStateHash, callback = (error, request) ->) ->
ClsiManager._getContentFromMongo project_id, (error, docs, files) ->
return callback(error) if error?
options = _.clone(options)
options.syncType = "full"
options.syncState = projectStateHash
ClsiManager._finaliseRequest project_id, options, project, docs, files, callback
_getContentFromMongo: (project_id, callback = (error, docs, files) ->) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
ProjectEntityHandler.getAllDocs project_id, (error, docs = {}) ->
return callback(error) if error?
ProjectEntityHandler.getAllFiles project_id, (error, files = {}) ->
return callback(error) if error?
callback(null, docs, files)
_finaliseRequest: (project_id, options, project, docs, files, callback = (error, params) -> ) ->
resources = []
rootResourcePath = null
rootResourcePathOverride = null
hasMainFile = false
numberOfDocsInProject = 0
for path, doc of docs
path = path.replace(/^\//, "") # Remove leading /
numberOfDocsInProject++
if doc.lines? # add doc to resources unless it is just a stub entry
resources.push
path: path
content: doc.lines.join("\n")
if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
rootResourcePath = path
if options.rootDoc_id? and doc._id.toString() == options.rootDoc_id.toString()
rootResourcePathOverride = path
if path is "main.tex"
hasMainFile = true
rootResourcePath = rootResourcePathOverride if rootResourcePathOverride?
if !rootResourcePath?
if hasMainFile
logger.warn {project_id}, "no root document found, setting to main.tex"
rootResourcePath = "main.tex"
else if numberOfDocsInProject is 1 # only one file, must be the main document
for path, doc of docs
rootResourcePath = path.replace(/^\//, "") # Remove leading /
logger.warn {project_id, rootResourcePath: rootResourcePath}, "no root document found, single document in project"
else
return callback new Error("no main file specified")
for path, file of files
path = path.replace(/^\//, "") # Remove leading /
resources.push
path: path
url: "#{Settings.apis.filestore.url}/project/#{project._id}/file/#{file._id}"
modified: file.created?.getTime()
callback null, {
compile:
options:
compiler: project.compiler
timeout: options.timeout
imageName: project.imageName
draft: !!options.draft
check: options.check
syncType: options.syncType
syncState: options.syncState
rootResourcePath: rootResourcePath
resources: resources
}
wordCount: (project_id, user_id, file, options, callback = (error, response) ->) ->
ClsiManager._buildRequest project_id, options, (error, req) ->
filename = file || req?.compile?.rootResourcePath
wordcount_url = ClsiManager._getCompilerUrl(options?.compileGroup, project_id, user_id, "wordcount")
opts =
url: wordcount_url
qs:
file: filename
image: req.compile.options.imageName
method: "GET"
ClsiManager._makeRequest project_id, opts, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
callback null, body
else
error = new Error("CLSI returned non-success code: #{response.statusCode}")
logger.error err: error, project_id: project_id, "CLSI returned failure code"
callback error, body

View File

@@ -1,35 +0,0 @@
Settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
crypto = require "crypto"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
# The "state" of a project is a hash of the relevant attributes in the
# project object in this case we only need the rootFolder.
#
# The idea is that it will change if any doc or file is
# created/renamed/deleted, and also if the content of any file (not
# doc) changes.
#
# When the hash changes the full set of files on the CLSI will need to
# be updated. If it doesn't change then we can overwrite changed docs
# in place on the clsi, getting them from the docupdater.
#
# The docupdater is responsible for setting the key in redis, and
# unsetting it if it removes any documents from the doc updater.
buildState = (s) ->
return crypto.createHash('sha1').update(s, 'utf8').digest('hex')
module.exports = ClsiStateManager =
computeHash: (project, options, callback = (err, hash) ->) ->
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
fileList = ("#{f.file._id}:#{f.file.rev}:#{f.file.created}:#{f.path}" for f in files or [])
docList = ("#{d.doc._id}:#{d.path}" for d in docs or [])
sortedEntityList = [docList..., fileList...].sort()
# ignore the isAutoCompile options as it doesn't affect the
# output, but include all other options e.g. draft
optionsList = ("option #{key}:#{value}" for key, value of options or {} when not (key in ['isAutoCompile']))
sortedOptionsList = optionsList.sort()
hash = buildState([sortedEntityList..., sortedOptionsList...].join("\n"))
callback(null, hash)

View File

@@ -1,280 +0,0 @@
Metrics = require "metrics-sharelatex"
ProjectGetter = require('../Project/ProjectGetter')
CompileManager = require("./CompileManager")
ClsiManager = require("./ClsiManager")
logger = require "logger-sharelatex"
request = require "request"
sanitize = require('sanitizer')
Settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
UserGetter = require "../User/UserGetter"
RateLimiter = require("../../infrastructure/RateLimiter")
ClsiCookieManager = require("./ClsiCookieManager")(Settings.apis.clsi?.backendGroupName)
Path = require("path")
module.exports = CompileController =
compile: (req, res, next = (error) ->) ->
res.setTimeout(5 * 60 * 1000)
project_id = req.params.Project_id
isAutoCompile = !!req.query?.auto_compile
user_id = AuthenticationController.getLoggedInUserId req
options = {
isAutoCompile: isAutoCompile
}
if req.body?.rootDoc_id?
options.rootDoc_id = req.body.rootDoc_id
else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.body?.check in ['validate', 'error', 'silent']
options.check = req.body.check
if req.body?.incrementalCompilesEnabled
options.incrementalCompilesEnabled = true
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
compileGroup: limits?.compileGroup
clsiServerId:clsiServerId
validationProblems:validationProblems
pdfDownloadDomain: Settings.pdfDownloadDomain
}
stopCompile: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = AuthenticationController.getLoggedInUserId req
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
CompileManager.stopCompile project_id, user_id, (error) ->
return next(error) if error?
res.status(200).send()
# Used for submissions through the public API
compileSubmission: (req, res, next = (error) ->) ->
res.setTimeout(5 * 60 * 1000)
submission_id = req.params.submission_id
options = {}
if req.body?.rootResourcePath?
options.rootResourcePath = req.body.rootResourcePath
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.body?.check in ['validate', 'error', 'silent']
options.check = req.body.check
options.compileGroup = req.body?.compileGroup || Settings.defaultFeatures.compileGroup
options.timeout = req.body?.timeout || Settings.defaultFeatures.compileTimeout
logger.log {options:options, submission_id:submission_id}, "got compileSubmission request"
ClsiManager.sendExternalRequest submission_id, req.body, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
return next(error) if error?
logger.log {submission_id:submission_id, files:outputFiles}, "compileSubmission output files"
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
clsiServerId: clsiServerId
validationProblems: validationProblems
}
_compileAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
user_id = AuthenticationController.getLoggedInUserId req
return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
_downloadAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
user_id = AuthenticationController.getLoggedInUserId req
return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
downloadPdf: (req, res, next = (error) ->)->
Metrics.inc "pdf-downloads"
project_id = req.params.Project_id
isPdfjsPartialDownload = req.query?.pdfng
rateLimit = (callback)->
if isPdfjsPartialDownload
callback null, true
else
rateLimitOpts =
endpointName: "full-pdf-download"
throttle: 1000
subjectName : req.ip
timeInterval : 60 * 60
RateLimiter.addCount rateLimitOpts, callback
ProjectGetter.getProject project_id, name: 1, (err, project) ->
res.contentType("application/pdf")
filename = "#{CompileController._getSafeProjectName(project)}.pdf"
if !!req.query.popupDownload
logger.log project_id: project_id, "download pdf as popup download"
res.setContentDisposition('attachment', {filename})
else
logger.log project_id: project_id, "download pdf to embed in browser"
res.setContentDisposition('', {filename})
rateLimit (err, canContinue)->
if err?
logger.err err:err, "error checking rate limit for pdf download"
return res.send 500
else if !canContinue
logger.log project_id:project_id, ip:req.ip, "rate limit hit downloading pdf"
res.send 500
else
CompileController._downloadAsUser req, (error, user_id) ->
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, "output.pdf"
CompileController.proxyToClsi(project_id, url, req, res, next)
_getSafeProjectName: (project) ->
safeProjectName = project.name.replace(new RegExp("\\W", "g"), '_')
sanitize.escape(safeProjectName)
deleteAuxFiles: (req, res, next) ->
project_id = req.params.Project_id
CompileController._compileAsUser req, (error, user_id) ->
return next(error) if error?
CompileManager.deleteAuxFiles project_id, user_id, (error) ->
return next(error) if error?
res.sendStatus(200)
# this is only used by templates, so is not called with a user_id
compileAndDownloadPdf: (req, res, next)->
project_id = req.params.project_id
# pass user_id as null, since templates are an "anonymous" compile
CompileManager.compile project_id, null, {}, (err)->
if err?
logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf"
res.sendStatus 500
url = "/project/#{project_id}/output/output.pdf"
CompileController.proxyToClsi project_id, url, req, res, next
getFileFromClsi: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
CompileController._downloadAsUser req, (error, user_id) ->
return next(error) if error?
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file
CompileController.proxyToClsi(project_id, url, req, res, next)
getFileFromClsiWithoutUser: (req, res, next = (error) ->) ->
submission_id = req.params.submission_id
url = CompileController._getFileUrl submission_id, null, req.params.build_id, req.params.file
limits = { compileGroup: req.body?.compileGroup || Settings.defaultFeatures.compileGroup }
CompileController.proxyToClsiWithLimits(submission_id, url, limits, req, res, next)
# compute a GET file url for a given project, user (optional), build (optional) and file
_getFileUrl: (project_id, user_id, build_id, file) ->
if user_id? and build_id?
url = "/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{file}"
else if user_id?
url = "/project/#{project_id}/user/#{user_id}/output/#{file}"
else if build_id?
url = "/project/#{project_id}/build/#{build_id}/output/#{file}"
else
url = "/project/#{project_id}/output/#{file}"
return url
# compute a POST url for a project, user (optional) and action
_getUrl: (project_id, user_id, action) ->
path = "/project/#{project_id}"
path += "/user/#{user_id}" if user_id?
return "#{path}/#{action}"
proxySyncPdf: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
{page, h, v} = req.query
if not page?.match(/^\d+$/)
return next(new Error("invalid page parameter"))
if not h?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid h parameter"))
if not v?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid v parameter"))
# whether this request is going to a per-user container
CompileController._compileAsUser req, (error, user_id) ->
return next(error) if error?
url = CompileController._getUrl(project_id, user_id, "sync/pdf")
destination = {url: url, qs: {page, h, v}}
CompileController.proxyToClsi(project_id, destination, req, res, next)
proxySyncCode: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
{file, line, column} = req.query
if not file?
return next(new Error("missing file parameter"))
# Check that we are dealing with a simple file path (this is not
# strictly needed because synctex uses this parameter as a label
# to look up in the synctex output, and does not open the file
# itself). Since we have valid synctex paths like foo/./bar we
# allow those by replacing /./ with /
testPath = file.replace '/./', '/'
if Path.resolve("/", testPath) isnt "/#{testPath}"
return next(new Error("invalid file parameter"))
if not line?.match(/^\d+$/)
return next(new Error("invalid line parameter"))
if not column?.match(/^\d+$/)
return next(new Error("invalid column parameter"))
CompileController._compileAsUser req, (error, user_id) ->
return next(error) if error?
url = CompileController._getUrl(project_id, user_id, "sync/code")
destination = {url:url, qs: {file, line, column}}
CompileController.proxyToClsi(project_id, destination, req, res, next)
proxyToClsi: (project_id, url, req, res, next = (error) ->) ->
if req.query?.compileGroup
CompileController.proxyToClsiWithLimits(project_id, url, {compileGroup: req.query.compileGroup}, req, res, next)
else
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return next(error) if error?
CompileController.proxyToClsiWithLimits(project_id, url, limits, req, res, next)
proxyToClsiWithLimits: (project_id, url, limits, req, res, next = (error) ->) ->
ClsiCookieManager.getCookieJar project_id, (err, jar)->
if err?
logger.err err:err, "error getting cookie jar for clsi request"
return callback(err)
# expand any url parameter passed in as {url:..., qs:...}
if typeof url is "object"
{url, qs} = url
compilerUrl = Settings.apis.clsi.url
url = "#{compilerUrl}#{url}"
logger.log url: url, "proxying to CLSI"
oneMinute = 60 * 1000
# the base request
options = { url: url, method: req.method, timeout: oneMinute, jar : jar }
# add any provided query string
options.qs = qs if qs?
# if we have a build parameter, pass it through to the clsi
if req.query?.pdfng && req.query?.build? # only for new pdf viewer
options.qs ?= {}
options.qs.build = req.query.build
# if we are byte serving pdfs, pass through If-* and Range headers
# do not send any others, there's a proxying loop if Host: is passed!
if req.query?.pdfng
newHeaders = {}
for h, v of req.headers
newHeaders[h] = req.headers[h] if /^(If-|Range)/i.test(h)
options.headers = newHeaders
proxy = request(options)
proxy.pipe(res)
proxy.on "error", (error) ->
logger.warn err: error, url: url, "CLSI proxy error"
wordCount: (req, res, next) ->
project_id = req.params.Project_id
file = req.query.file || false
CompileController._compileAsUser req, (error, user_id) ->
return next(error) if error?
CompileManager.wordCount project_id, user_id, file, (error, body) ->
return next(error) if error?
res.contentType("application/json")
res.send body

View File

@@ -1,109 +0,0 @@
Settings = require('settings-sharelatex')
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("clsi_recently_compiled")
ProjectGetter = require('../Project/ProjectGetter')
ProjectRootDocManager = require "../Project/ProjectRootDocManager"
UserGetter = require "../User/UserGetter"
ClsiManager = require "./ClsiManager"
Metrics = require('metrics-sharelatex')
logger = require("logger-sharelatex")
rateLimiter = require("../../infrastructure/RateLimiter")
module.exports = CompileManager =
compile: (project_id, user_id, options = {}, _callback = (error) ->) ->
timer = new Metrics.Timer("editor.compile")
callback = (args...) ->
timer.done()
_callback(args...)
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
logger.warn {project_id, user_id}, "project was recently compiled so not continuing"
return callback null, "too-recently-compiled", []
CompileManager._checkIfAutoCompileLimitHasBeenHit options.isAutoCompile, "everyone", (err, canCompile)->
if !canCompile
return callback null, "autocompile-backoff", []
ProjectRootDocManager.ensureRootDocumentIsSet project_id, (error) ->
return callback(error) if error?
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return callback(error) if error?
for key, value of limits
options[key] = value
# Put a lower limit on autocompiles for free users, based on compileGroup
CompileManager._checkCompileGroupAutoCompileLimit options.isAutoCompile, limits.compileGroup, (err, canCompile)->
if !canCompile
return callback null, "autocompile-backoff", []
# only pass user_id down to clsi if this is a per-user compile
compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
return callback(error) if error?
logger.log files: outputFiles, "output files"
callback(null, status, outputFiles, clsiServerId, limits, validationProblems)
stopCompile: (project_id, user_id, callback = (error) ->) ->
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return callback(error) if error?
ClsiManager.stopCompile project_id, user_id, limits, callback
deleteAuxFiles: (project_id, user_id, callback = (error) ->) ->
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return callback(error) if error?
ClsiManager.deleteAuxFiles project_id, user_id, limits, callback
getProjectCompileLimits: (project_id, callback = (error, limits) ->) ->
ProjectGetter.getProject project_id, owner_ref: 1, (error, project) ->
return callback(error) if error?
UserGetter.getUser project.owner_ref, {"features":1}, (err, owner)->
return callback(error) if error?
callback null, {
timeout: owner?.features?.compileTimeout || Settings.defaultFeatures.compileTimeout
compileGroup: owner?.features?.compileGroup || Settings.defaultFeatures.compileGroup
}
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
_checkCompileGroupAutoCompileLimit: (isAutoCompile, compileGroup, callback = (err, canCompile)->)->
if !isAutoCompile
return callback(null, true)
if compileGroup is "standard"
# apply extra limits to the standard compile group
CompileManager._checkIfAutoCompileLimitHasBeenHit isAutoCompile, compileGroup, callback
else
Metrics.inc "auto-compile-#{compileGroup}"
return callback(null, true) # always allow priority group users to compile
_checkIfAutoCompileLimitHasBeenHit: (isAutoCompile, compileGroup, callback = (err, canCompile)->)->
if !isAutoCompile
return callback(null, true)
Metrics.inc "auto-compile-#{compileGroup}"
opts =
endpointName:"auto_compile"
timeInterval:20
subjectName:compileGroup
throttle: Settings?.rateLimit?.autoCompile?[compileGroup] || 25
rateLimiter.addCount opts, (err, canCompile)->
if err?
canCompile = false
if !canCompile
Metrics.inc "auto-compile-#{compileGroup}-limited"
callback err, canCompile
wordCount: (project_id, user_id, file, callback = (error) ->) ->
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
return callback(error) if error?
ClsiManager.wordCount project_id, user_id, file, limits, callback

View File

@@ -1,42 +0,0 @@
AuthenticationController = require "../Authentication/AuthenticationController"
ContactManager = require "./ContactManager"
UserGetter = require "../User/UserGetter"
logger = require "logger-sharelatex"
Modules = require "../../infrastructure/Modules"
module.exports = ContactsController =
getContacts: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId req
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
return next(error) if error?
UserGetter.getUsers contact_ids, {
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
}, (error, contacts) ->
return next(error) if error?
# UserGetter.getUsers may not preserve order so put them back in order
positions = {}
for contact_id, i in contact_ids
positions[contact_id] = i
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
contacts = contacts.filter (c) -> !c.holdingAccount
contacts = contacts.map(ContactsController._formatContact)
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
return next(error) if error?
contacts = contacts.concat(additional_contacts...)
res.send({
contacts: contacts
})
_formatContact: (contact) ->
return {
id: contact._id?.toString()
email: contact.email || ""
first_name: contact.first_name || ""
last_name: contact.last_name || ""
type: "user"
}

View File

@@ -1,39 +0,0 @@
request = require "request"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
module.exports = ContactManager =
getContactIds: (user_id, options = { limits: 50 }, callback = (error, contacts) ->) ->
logger.log {user_id}, "getting user contacts"
url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts"
request.get {
url: url
qs: options
json: true
jar: false
}, (error, res, data) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, data?.contact_ids or [])
else
error = new Error("contacts api responded with non-success code: #{res.statusCode}")
logger.error {err: error, user_id}, "error getting contacts for user"
callback(error)
addContact: (user_id, contact_id, callback = (error) ->) ->
logger.log {user_id, contact_id}, "add user contact"
url = "#{settings.apis.contacts.url}/user/#{user_id}/contacts"
request.post {
url: url
json: {
contact_id: contact_id
}
jar: false
}, (error, res, data) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, data?.contact_ids or [])
else
error = new Error("contacts api responded with non-success code: #{res.statusCode}")
logger.error {err: error, user_id, contact_id}, "error adding contact for user"
callback(error)

View File

@@ -1,9 +0,0 @@
AuthenticationController = require('../Authentication/AuthenticationController')
ContactController = require "./ContactController"
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.get '/user/contacts',
AuthenticationController.requireLogin(),
ContactController.getContacts

View File

@@ -1,23 +0,0 @@
RedisWrapper = require('../../infrastructure/RedisWrapper')
rclient = RedisWrapper.client('cooldown')
logger = require('logger-sharelatex')
COOLDOWN_IN_SECONDS = 60 * 10
module.exports = CooldownManager =
_buildKey: (projectId) ->
"Cooldown:{#{projectId}}"
putProjectOnCooldown: (projectId, callback=(err)->) ->
logger.log {projectId}, "[Cooldown] putting project on cooldown for #{COOLDOWN_IN_SECONDS} seconds"
rclient.set(CooldownManager._buildKey(projectId), '1', 'EX', COOLDOWN_IN_SECONDS, callback)
isProjectOnCooldown: (projectId, callback=(err, isOnCooldown)->) ->
rclient.get CooldownManager._buildKey(projectId), (err, result) ->
if err?
return callback(err)
callback(null, result == "1")

View File

@@ -1,17 +0,0 @@
CooldownManager = require('./CooldownManager')
logger = require('logger-sharelatex')
module.exports = CooldownMiddleware =
freezeProject: (req, res, next) ->
projectId = req.params.Project_id
if !projectId?
return next(new Error('[Cooldown] No projectId parameter on route'))
CooldownManager.isProjectOnCooldown projectId, (err, projectIsOnCooldown) ->
if err?
return next(err)
if projectIsOnCooldown
logger.log {projectId}, "[Cooldown] project is on cooldown, denying request"
return res.sendStatus(429)
next()

View File

@@ -1,123 +0,0 @@
request = require("request").defaults(jar: false)
logger = require "logger-sharelatex"
settings = require "settings-sharelatex"
Errors = require "../Errors/Errors"
module.exports = DocstoreManager =
deleteDoc: (project_id, doc_id, callback = (error) ->) ->
logger.log project_id: project_id, doc_id: doc_id, "deleting doc in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
request.del url, (error, res, body) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null)
else if res.statusCode is 404
error = new Errors.NotFoundError("tried to delete doc not in docstore")
logger.error err: error, project_id: project_id, doc_id: doc_id, "tried to delete doc not in docstore"
callback(error) # maybe suppress the error when delete doc which is not present?
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, doc_id: doc_id, "error deleting doc in docstore"
callback(error)
getAllDocs: (project_id, callback = (error) ->) ->
logger.log project_id: project_id, "getting all docs for project in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc"
request.get {
url: url
json: true
}, (error, res, docs) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, docs)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, "error getting all docs from docstore"
callback(error)
getAllRanges: (project_id, callback = (error) ->) ->
logger.log { project_id }, "getting all doc ranges for project in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges"
request.get {
url: url
json: true
}, (error, res, docs) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, docs)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore"
callback(error)
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
if typeof(options) == "function"
callback = options
options = {}
logger.log project_id: project_id, doc_id: doc_id, options: options, "getting doc in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
if options.include_deleted
url += "?include_deleted=true"
request.get {
url: url
json: true
}, (error, res, doc) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
else if res.statusCode is 404
error = new Errors.NotFoundError("doc not found in docstore")
logger.error err: error, project_id: project_id, doc_id: doc_id, "doc not found in docstore"
callback(error)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
callback(error)
updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) ->
logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
request.post {
url: url
json:
lines: lines
version: version
ranges: ranges
}, (error, res, result) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
logger.log project_id: project_id, doc_id: doc_id, "update doc in docstore url finished"
callback(null, result.modified, result.rev)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, doc_id: doc_id, "error updating doc in docstore"
callback(error)
archiveProject: (project_id, callback)->
url = "#{settings.apis.docstore.url}/project/#{project_id}/archive"
logger.log project_id:project_id, "archiving project in docstore"
request.post url, (err, res, docs) ->
if err?
logger.err err:err, project_id:project_id, "error archving project in docstore"
return callback(err)
if 200 <= res.statusCode < 300
callback()
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.err err: error, project_id: project_id, "error archiving project in docstore"
return callback(error)
unarchiveProject: (project_id, callback)->
url = "#{settings.apis.docstore.url}/project/#{project_id}/unarchive"
logger.log project_id:project_id, "unarchiving project in docstore"
request.post url, (err, res, docs) ->
if err?
logger.err err:err, project_id:project_id, "error unarchiving project in docstore"
return callback(err)
if 200 <= res.statusCode < 300
callback()
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.err err: error, project_id: project_id, "error unarchiving project in docstore"
return callback(error)

View File

@@ -1,231 +0,0 @@
request = require 'request'
request = request.defaults()
settings = require 'settings-sharelatex'
_ = require 'underscore'
async = require 'async'
logger = require('logger-sharelatex')
metrics = require('metrics-sharelatex')
Project = require("../../models/Project").Project
module.exports = DocumentUpdaterHandler =
flushProjectToMongo: (project_id, callback = (error) ->)->
logger.log project_id:project_id, "flushing project from document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/flush"
method: "POST"
}, project_id, "flushing.mongo.project", callback
flushMultipleProjectsToMongo: (project_ids, callback = (error) ->) ->
jobs = []
for project_id in project_ids
do (project_id) ->
jobs.push (callback) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, callback
async.series jobs, callback
flushProjectToMongoAndDelete: (project_id, callback = ()->) ->
timer = new metrics.Timer("delete.mongo.project")
url = "#{settings.apis.documentupdater.url}"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}"
method: "DELETE"
}, project_id, "flushing.mongo.project", callback
flushDocToMongo: (project_id, doc_id, callback = (error) ->) ->
logger.log project_id:project_id, doc_id: doc_id, "flushing doc from document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}/flush"
method: "POST"
}, project_id, "flushing.mongo.doc", callback
deleteDoc : (project_id, doc_id, callback = ()->)->
logger.log project_id:project_id, doc_id: doc_id, "deleting doc from document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}"
method: "DELETE"
}, project_id, "delete.mongo.doc", callback
getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
json: true
}, project_id, "get-document", (error, doc) ->
return callback(error) if error?
callback null, doc.lines, doc.version, doc.ranges, doc.ops
setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)->
logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}"
method: "POST"
json:
lines: docLines
source: source
user_id: user_id
}, project_id, "set-document", callback
getProjectDocsIfMatch: (project_id, projectStateHash, callback = (error, docs) ->) ->
# If the project state hasn't changed, we can get all the latest
# docs from redis via the docupdater. Otherwise we will need to
# fall back to getting them from mongo.
timer = new metrics.Timer("get-project-docs")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/get_and_flush_if_old?state=#{projectStateHash}"
logger.log project_id:project_id, "getting project docs from document updater"
request.post url, (error, res, body)->
timer.done()
if error?
logger.error err:error, url:url, project_id:project_id, "error getting project docs from doc updater"
return callback(error)
if res.statusCode is 409 # HTTP response code "409 Conflict"
# Docupdater has checked the projectStateHash and found that
# it has changed. This means that the docs currently in redis
# aren't the only change to the project and the full set of
# docs/files should be retreived from docstore/filestore
# instead.
return callback()
else if res.statusCode >= 200 and res.statusCode < 300
logger.log project_id:project_id, "got project docs from document document updater"
try
docs = JSON.parse(body)
catch error
return callback(error)
callback null, docs
else
logger.error project_id:project_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}")
clearProjectState: (project_id, callback = (error) ->) ->
logger.log project_id:project_id, "clearing project state from document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/clearState"
method: "POST"
}, project_id, "clear-project-state", callback
acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) ->
logger.log {project_id, doc_id }, "accepting #{ change_ids.length } changes"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}/change/accept"
json:
change_ids: change_ids
method: "POST"
}, project_id, "accept-changes", callback
deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
timer = new metrics.Timer("delete-thread")
logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
method: "DELETE"
}, project_id, "delete-thread", callback
resyncProjectHistory: (project_id, projectHistoryId, docs, files, callback) ->
logger.info {project_id, docs, files}, "resyncing project history in doc updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}/history/resync"
json: { docs, files, projectHistoryId }
method: "POST"
}, project_id, "resync-project-history", callback
updateProjectStructure: (project_id, projectHistoryId, userId, changes, callback = (error) ->)->
return callback() if !settings.apis.project_history?.sendProjectStructureOps
docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs)
fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles)
projectVersion = changes?.newProject?.version
return callback() if (docUpdates.length + fileUpdates.length) < 1
if !projectVersion?
logger.error {project_id, changes, projectVersion}, "did not receive project version in changes"
return callback(new Error("did not receive project version in changes"))
logger.log {project_id}, "updating project structure in doc updater"
DocumentUpdaterHandler._makeRequest {
path: "/project/#{project_id}"
json: {
docUpdates,
fileUpdates,
userId,
version: projectVersion
projectHistoryId
}
method: "POST"
}, project_id, "update-project-structure", callback
_makeRequest: (options, project_id, metricsKey, callback) ->
timer = new metrics.Timer(metricsKey)
request {
url: "#{settings.apis.documentupdater.url}#{options.path}"
json: options.json
method: options.method || "GET"
}, (error, res, body)->
timer.done()
if error?
logger.error {error, project_id}, "error making request to document updater"
callback error
else if res.statusCode >= 200 and res.statusCode < 300
callback null, body
else
error = new Error("document updater returned a failure status code: #{res.statusCode}")
logger.error {error, project_id}, "document updater returned failure status code: #{res.statusCode}"
callback error
_getUpdates: (entityType, oldEntities, newEntities) ->
oldEntities ||= []
newEntities ||= []
updates = []
oldEntitiesHash = _.indexBy oldEntities, (entity) -> entity[entityType]._id.toString()
newEntitiesHash = _.indexBy newEntities, (entity) -> entity[entityType]._id.toString()
# Send deletes before adds (and renames) to keep a 1:1 mapping between
# paths and ids
#
# When a file is replaced, we first delete the old file and then add the
# new file. If the 'add' operation is sent to project history before the
# 'delete' then we would have two files with the same path at that point
# in time.
for id, oldEntity of oldEntitiesHash
newEntity = newEntitiesHash[id]
if !newEntity?
# entity deleted
updates.push
id: id
pathname: oldEntity.path
newPathname: ''
for id, newEntity of newEntitiesHash
oldEntity = oldEntitiesHash[id]
if !oldEntity?
# entity added
updates.push
id: id
pathname: newEntity.path
docLines: newEntity.docLines
url: newEntity.url
hash: newEntity.file?.hash
else if newEntity.path != oldEntity.path
# entity renamed
updates.push
id: id
pathname: oldEntity.path
newPathname: newEntity.path
updates
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

@@ -1,48 +0,0 @@
ProjectGetter = require "../Project/ProjectGetter"
ProjectLocator = require "../Project/ProjectLocator"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
logger = require("logger-sharelatex")
module.exports =
getDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
plain = req?.query?.plain == 'true'
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
ProjectGetter.getProject project_id, rootFolder: true, overleaf: true, (error, project) ->
return next(error) if error?
return res.sendStatus(404) if !project?
ProjectLocator.findElement {project: project, element_id: doc_id, type: 'doc'}, (error, doc, path) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding doc contents for getDocument"
return next(error)
if plain
res.type "text/plain"
res.send lines.join('\n')
else
projectHistoryId = project?.overleaf?.history?.id
res.json {
lines: lines
version: version
ranges: ranges
pathname: path.fileSystem
projectHistoryId: projectHistoryId
}
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
{lines, version, ranges, lastUpdatedAt, lastUpdatedBy} = req.body
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
ProjectEntityUpdateHandler.updateDocLines project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, (error) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
logger.log doc_id:doc_id, project_id:project_id, "finished receiving set document request from api (docupdater)"
res.sendStatus 200

View File

@@ -1,34 +0,0 @@
module.exports = DocumentHelper =
getTitleFromTexContent: (content, maxContentToScan = 30000) ->
TITLE_WITH_CURLY_BRACES = /\\[tT]itle\*?\s*{([^}]+)}/
TITLE_WITH_SQUARE_BRACES = /\\[tT]itle\s*\[([^\]]+)\]/
for line in DocumentHelper._getLinesFromContent(content, maxContentToScan)
if match = line.match(TITLE_WITH_CURLY_BRACES) || line.match(TITLE_WITH_SQUARE_BRACES)
return DocumentHelper.detex(match[1])
return null
contentHasDocumentclass: (content, maxContentToScan = 30000) ->
for line in DocumentHelper._getLinesFromContent(content, maxContentToScan)
# We've had problems with this regex locking up CPU.
# Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :()
# This regex will only look from the start of the line, including whitespace so will return quickly
# regardless of line length.
return true if line.match /^\s*\\documentclass/
return false
detex: (string) ->
return string.replace(/\\LaTeX/g, 'LaTeX')
.replace(/\\TeX/g, 'TeX')
.replace(/\\TikZ/g, 'TikZ')
.replace(/\\BibTeX/g, 'BibTeX')
.replace(/\\\[[A-Za-z0-9. ]*\]/g, ' ') # line spacing
.replace(/\\(?:[a-zA-Z]+|.|)/g, '')
.replace(/{}|~/g, ' ')
.replace(/[${}]/g, '')
.replace(/ +/g, ' ')
.trim()
_getLinesFromContent: (content, maxContentToScan) ->
return if typeof content is 'string' then content.substring(0, maxContentToScan).split("\n") else content

View File

@@ -1,40 +0,0 @@
logger = require "logger-sharelatex"
Metrics = require "metrics-sharelatex"
ProjectGetter = require('../Project/ProjectGetter')
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?
ProjectGetter.getProject project_id, name: true, (error, project) ->
return next(error) if error?
ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) ->
return next(error) if error?
res.setContentDisposition(
'attachment',
{filename: "#{project.name}.zip"}
)
res.contentType('application/zip')
stream.pipe(res)
downloadMultipleProjects: (req, res, next) ->
project_ids = req.query.project_ids.split(",")
Metrics.inc "zip-downloads-multiple"
logger.log project_ids: project_ids, "downloading multiple projects"
DocumentUpdaterHandler.flushMultipleProjectsToMongo project_ids, (error) ->
return next(error) if error?
ProjectZipStreamManager.createZipStreamForMultipleProjects project_ids, (error, stream) ->
return next(error) if error?
res.setContentDisposition(
'attachment',
{filename: "Overleaf Projects (#{project_ids.length} items).zip"}
)
res.contentType('application/zip')
stream.pipe(res)

View File

@@ -1,80 +0,0 @@
archiver = require "archiver"
async = require "async"
logger = require "logger-sharelatex"
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
ProjectGetter = require('../Project/ProjectGetter')
FileStoreHandler = require("../FileStore/FileStoreHandler")
module.exports = ProjectZipStreamManager =
createZipStreamForMultipleProjects: (project_ids, callback = (error, stream) ->) ->
# We'll build up a zip file that contains multiple zip files
archive = archiver("zip")
archive.on "error", (err)->
logger.err err:err, project_ids:project_ids, "something went wrong building archive of project"
callback null, archive
logger.log project_ids: project_ids, "creating zip stream of multiple projects"
jobs = []
for project_id in project_ids or []
do (project_id) ->
jobs.push (callback) ->
ProjectGetter.getProject project_id, name: true, (error, project) ->
return callback(error) if error?
logger.log project_id: project_id, name: project.name, "appending project to zip stream"
ProjectZipStreamManager.createZipStreamForProject project_id, (error, stream) ->
return callback(error) if error?
archive.append stream, name: "#{project.name}.zip"
stream.on "end", () ->
logger.log project_id: project_id, name: project.name, "zip stream ended"
callback()
async.series jobs, () ->
logger.log project_ids: project_ids, "finished creating zip stream of multiple projects"
archive.finalize()
createZipStreamForProject: (project_id, callback = (error, stream) ->) ->
archive = archiver("zip")
# return stream immediately before we start adding things to it
archive.on "error", (err)->
logger.err err:err, project_id:project_id, "something went wrong building archive of project"
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
stream.on "end", () ->
callback()
async.parallelLimit jobs, 5, callback

View File

@@ -1,217 +0,0 @@
logger = require('logger-sharelatex')
Metrics = require('metrics-sharelatex')
sanitize = require('sanitizer')
ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
EditorRealTimeController = require("./EditorRealTimeController")
async = require('async')
PublicAccessLevels = require("../Authorization/PublicAccessLevels")
_ = require('underscore')
module.exports = EditorController =
addDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (error, doc)->)->
EditorController.addDocWithRanges(project_id, folder_id, docName, docLines, {}, source, user_id, callback)
addDocWithRanges: (project_id, folder_id, docName, docLines, docRanges, source, user_id, callback = (error, doc)->)->
docName = docName.trim()
logger.log {project_id, folder_id, docName, source}, "sending new doc to project"
Metrics.inc "editor.add-doc"
ProjectEntityUpdateHandler.addDocWithRanges project_id, folder_id, docName, docLines, docRanges, user_id, (err, doc, folder_id)=>
if err?
logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock"
return callback(err)
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source)
callback(err, doc)
addFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (error, file)->)->
fileName = fileName.trim()
logger.log {project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id}, "sending new file to project"
Metrics.inc "editor.add-file"
ProjectEntityUpdateHandler.addFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, fileRef, folder_id)=>
if err?
logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock"
return callback(err)
EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source, linkedFileData)
callback(err, fileRef)
upsertDoc: (project_id, folder_id, docName, docLines, source, user_id, callback = (err)->)->
ProjectEntityUpdateHandler.upsertDoc project_id, folder_id, docName, docLines, source, user_id, (err, doc, didAddNewDoc) ->
if didAddNewDoc
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source)
callback err, doc
upsertFile: (project_id, folder_id, fileName, fsPath, linkedFileData, source, user_id, callback = (err, file) ->) ->
ProjectEntityUpdateHandler.upsertFile project_id, folder_id, fileName, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile) ->
return callback(err) if err?
if not didAddFile # replacement, so remove the existing file from the client
EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source
# now add the new file on the client
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', folder_id, newFile, source, linkedFileData
callback null, newFile
upsertDocWithPath: (project_id, elementPath, docLines, source, user_id, callback) ->
ProjectEntityUpdateHandler.upsertDocWithPath project_id, elementPath, docLines, source, user_id, (err, doc, didAddNewDoc, newFolders, lastFolder) ->
return callback(err) if err?
EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) ->
return callback(err) if err?
if didAddNewDoc
EditorRealTimeController.emitToRoom project_id, 'reciveNewDoc', lastFolder._id, doc, source
callback()
upsertFileWithPath: (project_id, elementPath, fsPath, linkedFileData, source, user_id, callback) ->
ProjectEntityUpdateHandler.upsertFileWithPath project_id, elementPath, fsPath, linkedFileData, user_id, (err, newFile, didAddFile, existingFile, newFolders, lastFolder) ->
return callback(err) if err?
EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) ->
return callback(err) if err?
if not didAddFile # replacement, so remove the existing file from the client
EditorRealTimeController.emitToRoom project_id, 'removeEntity', existingFile._id, source
# now add the new file on the client
EditorRealTimeController.emitToRoom project_id, 'reciveNewFile', lastFolder._id, newFile, source, linkedFileData
callback()
addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)->
folderName = folderName.trim()
logger.log {project_id, folder_id, folderName, source}, "sending new folder to project"
Metrics.inc "editor.add-folder"
ProjectEntityUpdateHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
if err?
logger.err {err, project_id, folder_id, folderName, source}, "could not add folder"
return callback(err)
EditorController._notifyProjectUsersOfNewFolder project_id, folder_id, folder, (err) ->
return callback(err) if err?
callback null, folder
mkdirp : (project_id, path, callback = (error, newFolders, lastFolder)->)->
logger.log project_id:project_id, path:path, "making directories if they don't exist"
ProjectEntityUpdateHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=>
if err?
logger.err err:err, project_id:project_id, path:path, "could not mkdirp"
return callback(err)
EditorController._notifyProjectUsersOfNewFolders project_id, newFolders, (err) ->
return callback(err) if err?
callback null, newFolders, lastFolder
deleteEntity : (project_id, entity_id, entityType, source, userId, callback = (error)->)->
logger.log {project_id, entity_id, entityType, source}, "start delete process of entity"
Metrics.inc "editor.delete-entity"
ProjectEntityUpdateHandler.deleteEntity project_id, entity_id, entityType, userId, (err)->
if err?
logger.err {err, project_id, entity_id, entityType}, "could not delete entity"
return callback(err)
logger.log {project_id, entity_id, entityType}, "telling users entity has been deleted"
EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source)
callback()
deleteEntityWithPath: (project_id, path, source, user_id, callback) ->
ProjectEntityUpdateHandler.deleteEntityWithPath project_id, path, user_id, (err, entity_id) ->
return callback(err) if err?
EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source)
callback null, entity_id
notifyUsersProjectHasBeenDeletedOrRenamed: (project_id, callback)->
EditorRealTimeController.emitToRoom(project_id, 'projectRenamedOrDeletedByExternalSource')
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()
deleteProject: (project_id, callback)->
Metrics.inc "editor.delete-project"
logger.log project_id:project_id, "recived message to delete project"
ProjectDeleter.deleteProject project_id, callback
renameEntity: (project_id, entity_id, entityType, newName, userId, callback = (error) ->)->
newName = sanitize.escape(newName)
Metrics.inc "editor.rename-entity"
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
ProjectEntityUpdateHandler.renameEntity project_id, entity_id, entityType, newName, userId, (err) ->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity"
return callback(err)
if newName.length > 0
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
callback()
moveEntity: (project_id, entity_id, folder_id, entityType, userId, callback = (error) ->)->
Metrics.inc "editor.move-entity"
ProjectEntityUpdateHandler.moveEntity project_id, entity_id, folder_id, entityType, userId, (err) ->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity"
return callback(err)
EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
callback()
renameProject: (project_id, newName, callback = (err) ->) ->
ProjectDetailsHandler.renameProject project_id, newName, (err) ->
if err?
logger.err err:err, project_id:project_id, newName:newName, "error renaming project"
return callback(err)
EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', newName
callback()
setCompiler : (project_id, compiler, callback = (err) ->) ->
ProjectOptionsHandler.setCompiler project_id, compiler, (err) ->
return callback(err) if err?
logger.log compiler:compiler, project_id:project_id, "setting compiler"
EditorRealTimeController.emitToRoom project_id, 'compilerUpdated', compiler
callback()
setImageName : (project_id, imageName, callback = (err) ->) ->
ProjectOptionsHandler.setImageName project_id, imageName, (err) ->
return callback(err) if err?
logger.log imageName:imageName, project_id:project_id, "setting imageName"
EditorRealTimeController.emitToRoom project_id, 'imageNameUpdated', imageName
callback()
setSpellCheckLanguage : (project_id, languageCode, callback = (err) ->) ->
ProjectOptionsHandler.setSpellCheckLanguage project_id, languageCode, (err) ->
return callback(err) if err?
logger.log languageCode:languageCode, project_id:project_id, "setting languageCode for spell check"
EditorRealTimeController.emitToRoom project_id, 'spellCheckLanguageUpdated', languageCode
callback()
setPublicAccessLevel : (project_id, newAccessLevel, callback = (err) ->) ->
ProjectDetailsHandler.setPublicAccessLevel project_id, newAccessLevel, (err) ->
return callback(err) if err?
EditorRealTimeController.emitToRoom(
project_id,
'project:publicAccessLevel:changed',
{newAccessLevel}
)
if newAccessLevel == PublicAccessLevels.TOKEN_BASED
ProjectDetailsHandler.ensureTokensArePresent project_id, (err, tokens) ->
return callback(err) if err?
EditorRealTimeController.emitToRoom(
project_id,
'project:tokens:changed',
{tokens}
)
callback()
else
callback()
setRootDoc: (project_id, newRootDocID, callback = (err) ->) ->
ProjectEntityUpdateHandler.setRootDoc project_id, newRootDocID, (err) ->
return callback(err) if err?
EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID
callback()
_notifyProjectUsersOfNewFolders: (project_id, folders, callback = (error)->)->
async.eachSeries folders,
(folder, cb) -> EditorController._notifyProjectUsersOfNewFolder project_id, folder.parentFolder_id, folder, cb
callback
_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

@@ -1,134 +0,0 @@
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
ProjectDeleter = require "../Project/ProjectDeleter"
logger = require "logger-sharelatex"
EditorRealTimeController = require "./EditorRealTimeController"
EditorController = require "./EditorController"
ProjectGetter = require('../Project/ProjectGetter')
UserGetter = require('../User/UserGetter')
AuthorizationManager = require("../Authorization/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('metrics-sharelatex')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = EditorHttpController =
joinProject: (req, res, next) ->
project_id = req.params.Project_id
user_id = req.query.user_id
if user_id == "anonymous-user"
user_id = null
logger.log {user_id, project_id}, "join project request"
Metrics.inc "editor.join-project"
EditorHttpController._buildJoinProjectView req, project_id, user_id, (error, project, privilegeLevel) ->
return next(error) if error?
# Hide access tokens if this is not the project owner
TokenAccessHandler.protectTokens(project, privilegeLevel)
res.json {
project: project
privilegeLevel: privilegeLevel
}
# Only show the 'renamed or deleted' message once
if project?.deletedByExternalDataSource
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
_buildJoinProjectView: (req, project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
logger.log {project_id, user_id}, "building the joinProject view"
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
token = TokenAccessHandler.getRequestToken(req, project_id)
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) ->
return callback(error) if error?
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
callback(null,
ProjectEditorHandler.buildProjectModelView(project, members, invites),
privilegeLevel
)
_nameIsAcceptableLength: (name)->
return name? and name.length < 150 and name.length != 0
addDoc: (req, res, next) ->
project_id = req.params.Project_id
name = req.body.name
parent_folder_id = req.body.parent_folder_id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project"
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
EditorController.addDoc project_id, parent_folder_id, name, [], "editor", user_id, (error, doc) ->
if error == "project_has_to_many_files"
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
else if error?
next(error)
else
res.json doc
addFolder: (req, res, next) ->
project_id = req.params.Project_id
name = req.body.name
parent_folder_id = req.body.parent_folder_id
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
if error == "project_has_to_many_files"
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
else if error?.message == 'invalid element name'
res.status(400).json(req.i18n.translate('invalid_file_name'))
else if error?
next(error)
else
res.json doc
renameEntity: (req, res, next) ->
project_id = req.params.Project_id
entity_id = req.params.entity_id
entity_type = req.params.entity_type
name = req.body.name
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.renameEntity project_id, entity_id, entity_type, name, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
moveEntity: (req, res, next) ->
project_id = req.params.Project_id
entity_id = req.params.entity_id
entity_type = req.params.entity_type
folder_id = req.body.folder_id
user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.moveEntity project_id, entity_id, folder_id, entity_type, user_id, (error) ->
return next(error) if error?
res.sendStatus 204
deleteDoc: (req, res, next)->
req.params.entity_type = "doc"
EditorHttpController.deleteEntity(req, res, next)
deleteFile: (req, res, next)->
req.params.entity_type = "file"
EditorHttpController.deleteEntity(req, res, next)
deleteFolder: (req, res, next)->
req.params.entity_type = "folder"
EditorHttpController.deleteEntity(req, res, next)
deleteEntity: (req, res, next) ->
project_id = req.params.Project_id
entity_id = req.params.entity_id
entity_type = req.params.entity_type
user_id = AuthenticationController.getLoggedInUserId(req)
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", user_id, (error) ->
return next(error) if error?
res.sendStatus 204

View File

@@ -1,23 +0,0 @@
Settings = require 'settings-sharelatex'
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("realtime")
os = require "os"
crypto = require "crypto"
HOST = os.hostname()
RND = crypto.randomBytes(4).toString('hex') # generate a random key for this process
COUNT = 0
module.exports = EditorRealTimeController =
emitToRoom: (room_id, message, payload...) ->
# create a unique message id using a counter
message_id = "web:#{HOST}:#{RND}-#{COUNT++}"
rclient.publish "editor-events", JSON.stringify
room_id: room_id
message: message
payload: payload
_id: message_id
emitToAll: (message, payload...) ->
@emitToRoom "all", message, payload...

View File

@@ -1,39 +0,0 @@
EditorHttpController = require('./EditorHttpController')
AuthenticationController = require "../Authentication/AuthenticationController"
AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.post '/project/:Project_id/doc', AuthorizationMiddleware.ensureUserCanWriteProjectContent,
RateLimiterMiddleware.rateLimit({
endpointName: "add-doc-to-project"
params: ["Project_id"]
maxRequests: 30
timeInterval: 60
}), EditorHttpController.addDoc
webRouter.post '/project/:Project_id/folder', AuthorizationMiddleware.ensureUserCanWriteProjectContent,
RateLimiterMiddleware.rateLimit({
endpointName: "add-folder-to-project"
params: ["Project_id"]
maxRequests: 60
timeInterval: 60
}), EditorHttpController.addFolder
webRouter.post '/project/:Project_id/:entity_type/:entity_id/rename', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.renameEntity
webRouter.post '/project/:Project_id/:entity_type/:entity_id/move', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.moveEntity
webRouter.delete '/project/:Project_id/file/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteFile
webRouter.delete '/project/:Project_id/doc/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteDoc
webRouter.delete '/project/:Project_id/folder/:entity_id', AuthorizationMiddleware.ensureUserCanWriteProjectContent, EditorHttpController.deleteFolder
# Called by the real-time API to load up the current project state.
# This is a post request because it's more than just a getting of data. We take actions
# whenever a user joins a project, like updating the deleted status.
apiRouter.post '/project/:Project_id/join', AuthenticationController.httpAuth,
RateLimiterMiddleware.rateLimit({
endpointName: "join-project"
params: ["Project_id"]
maxRequests: 30
timeInterval: 60
}), EditorHttpController.joinProject

View File

@@ -1,230 +0,0 @@
_ = require('underscore')
settings = require("settings-sharelatex")
marked = require('marked')
StringHelper = require "../Helpers/StringHelper"
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
BaseWithHeaderEmailLayout = require("./Layouts/" + settings.brandPrefix + "BaseWithHeaderEmailLayout")
SpamSafe = require("./SpamSafe")
SingleCTAEmailBody = require("./Bodies/" + settings.brandPrefix + "SingleCTAEmailBody")
CTAEmailTemplate = (content) ->
content.greeting ?= () -> 'Hi,'
content.secondaryMessage ?= () -> ""
return {
subject: (opts) -> content.subject(opts),
layout: BaseWithHeaderEmailLayout,
plainTextTemplate: (opts) -> """
#{content.greeting(opts)}
#{content.message(opts).trim()}
#{content.ctaText(opts)}: #{content.ctaURL(opts)}
#{content.secondaryMessage?(opts).trim() or ""}
Regards,
The #{settings.appName} Team - #{settings.siteUrl}
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: content.title?(opts)
greeting: content.greeting(opts)
message: marked(content.message(opts).trim())
secondaryMessage: marked(content.secondaryMessage(opts).trim())
ctaText: content.ctaText(opts)
ctaURL: content.ctaURL(opts)
gmailGoToAction: content.gmailGoToAction?(opts)
StringHelper: StringHelper
})
}
templates = {}
templates.accountMergeToOverleafAddress = CTAEmailTemplate({
subject: () -> "Confirm Account Merge - #{settings.appName}"
title: () -> "Confirm Account Merge"
message: () ->
"""
To merge your ShareLaTeX and Overleaf accounts, click the button below.
If you think you have received this message in error,
please contact us at https://www.overleaf.com/contact
"""
ctaText: () -> "Confirm Account Merge"
ctaURL: (opts) -> opts.tokenLinkUrl
})
templates.accountMergeToSharelatexAddress = templates.accountMergeToOverleafAddress
templates.registered = CTAEmailTemplate({
subject: () -> "Activate your #{settings.appName} Account"
message: (opts) -> """
Congratulations, you've just had an account created for you on #{settings.appName} with the email address '#{_.escape(opts.to)}'.
Click here to set your password and log in:
"""
secondaryMessage: () -> "If you have any questions or problems, please contact #{settings.adminEmail}"
ctaText: () -> "Set password"
ctaURL: (opts) -> opts.setNewPasswordUrl
})
templates.canceledSubscription = CTAEmailTemplate({
subject: () -> "#{settings.appName} thoughts"
message: () -> """
I'm sorry to see you cancelled your #{settings.appName} premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?
"""
secondaryMessage: () -> "Thank you in advance!"
ctaText: () -> "Leave Feedback"
ctaURL: (opts) -> "https://docs.google.com/forms/d/e/1FAIpQLScqU6Je1r4Afz6ul6oY0RAfN7RabdWv_oL1u7Rj1YBmXS4fiQ/viewform?usp=sf_link"
})
templates.reactivatedSubscription = CTAEmailTemplate({
subject: () -> "Subscription Reactivated - #{settings.appName}"
message: (opts) -> """
Your subscription was reactivated successfully.
"""
ctaText: () -> "View Subscription Dashboard"
ctaURL: (opts) -> "#{settings.siteUrl}/user/subscription"
})
templates.passwordResetRequested = CTAEmailTemplate({
subject: () -> "Password Reset - #{settings.appName}"
title: () -> "Password Reset"
message: () -> "We got a request to reset your #{settings.appName} password."
secondaryMessage: () -> """
If you ignore this message, your password won't be changed.
If you didn't request a password reset, let us know.
"""
ctaText: () -> "Reset password"
ctaURL: (opts) -> opts.setNewPasswordUrl
})
templates.confirmEmail = CTAEmailTemplate({
subject: () -> "Confirm Email - #{settings.appName}"
title: () -> "Confirm Email"
message: () -> "Please confirm your email on #{settings.appName}."
ctaText: () -> "Confirm Email"
ctaURL: (opts) -> opts.confirmEmailUrl
})
templates.projectInvite = CTAEmailTemplate({
subject: (opts) -> "#{ _.escape(SpamSafe.safeProjectName(opts.project.name, "New Project")) } - shared by #{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) }"
title: (opts) -> "#{ _.escape(SpamSafe.safeProjectName(opts.project.name, "New Project")) } - shared by #{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) }"
message: (opts) -> "#{ _.escape(SpamSafe.safeEmail(opts.owner.email, "a collaborator")) } wants to share #{ _.escape(SpamSafe.safeProjectName(opts.project.name, "a new project")) } with you."
ctaText: () -> "View project"
ctaURL: (opts) -> opts.inviteUrl
gmailGoToAction: (opts) ->
target: opts.inviteUrl
name: "View project"
description: "Join #{ _.escape(SpamSafe.safeProjectName(opts.project.name, "project")) } at #{ settings.appName }"
})
templates.verifyEmailToJoinTeam = CTAEmailTemplate({
subject: (opts) -> "#{ _.escape(SpamSafe.safeUserName(opts.inviterName, "A collaborator")) } has invited you to join a team on #{ settings.appName }"
title: (opts) -> "#{ _.escape(SpamSafe.safeUserName(opts.inviterName, "A collaborator")) } has invited you to join a team on #{ settings.appName }"
message: (opts) -> "Please click the button below to join the team and enjoy the benefits of an upgraded #{ settings.appName } account."
ctaText: (opts) -> "Join now"
ctaURL: (opts) -> opts.acceptInviteUrl
})
templates.dropboxUnlinkedDuplicate = CTAEmailTemplate({
subject: () -> "Your Dropbox Account has been Unlinked - #{settings.appName}"
message: (opts) -> """
Our automated systems have detected that your Dropbox account was linked to more than one Overleaf accounts. This should not have been allowed and might be causing issues with the Dropbox sync feature.
We have now unlinked all your Dropbox and Overleaf Accounts. To ensure your project will keep syncing you can link your Dropbox account to the Overleaf account of your choice now.
"""
ctaText: () -> "Link Dropbox Account"
ctaURL: (opts) -> "#{settings.siteUrl}/user/settings"
})
templates.testEmail = CTAEmailTemplate({
subject: () -> "A Test Email from #{settings.appName}"
title: () -> "A Test Email from #{settings.appName}"
greeting: () -> "Hi,"
message: () -> "This is a test Email from #{settings.appName}"
ctaText: () -> "Open #{settings.appName}"
ctaURL: () -> settings.siteUrl
})
templates.projectsTransferredFromSharelatex = CTAEmailTemplate({
subject: () -> "ShareLaTeX projects transferred to your Overleaf account"
title: () -> "ShareLaTeX projects transferred to your Overleaf account"
message: (opts) -> """
We are writing with important information about your Overleaf and ShareLaTeX accounts.
As part of our ongoing work to [integrate Overleaf and ShareLaTeX](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf),
we found a ShareLaTeX account with the email address #{opts.to} that matches your Overleaf account.
We have now transferred the projects from this ShareLaTeX account into your Overleaf account, so you may notice some new
projects on your Overleaf projects page.
When you next log in, you may be prompted to reconfirm your email address in order to regain access to your account.
If you have any questions, please contact our support team by reply.
"""
ctaText: () -> "Log in to #{ settings.appName }"
ctaURL: () -> settings.siteUrl + "/login"
})
templates.emailAddressPoachedEmail = CTAEmailTemplate({
subject: () -> "One of your email addresses has been moved to another #{ settings.appName } account"
title: () -> "One of your email addresses has been moved to another #{ settings.appName } account"
message: (opts) ->
message = """
We are writing with important information about your Overleaf account.
You added the email address #{opts.poached} to your #{opts.to} Overleaf account as a secondary (or affiliation)
email address, but we have had to remove it.
This is because your #{opts.poached} email address was also in use as the primary email address for an older Overleaf
account from before our [integration with ShareLaTeX to create Overleaf v2](https://www.overleaf.com/blog/518-exciting-news-sharelatex-is-joining-overleaf).
### What do I need to do?
You now have two Overleaf accounts, one under #{opts.poached} and one under #{opts.to}.
You may wish to log in to Overleaf as #{opts.poached} to check whether you have projects there that you would like to
keep. If you are not sure of the password, you can send yourself a password reset email to #{opts.poached}, via
https://www.overleaf.com/user/password/reset
Once you have downloaded your projects, you may wish to delete your
#{opts.poached} Overleaf account, which you can do from your account settings. You will then be able to add
#{opts.poached} as a secondary email address on your #{opts.to} account again.
"""
if opts.proFeatures
message += """
Because your #{opts.poached} email address was an institutional affiliation through which you had Pro features. Your Pro
features have been transferred to your #{opts.poached} account. If you would like to transfer them back to your
#{opts.to} account, you will need to delete the #{opts.poached} account and re-add it as a secondary email address,
as described above.
"""
message += """
If you have any questions, you can contact our support team by reply.
"""
return message
ctaText: () -> "Log in to #{ settings.appName }"
ctaURL: () -> settings.siteUrl + "/login"
})
module.exports =
templates: templates
CTAEmailTemplate: CTAEmailTemplate
buildEmail: (templateName, opts)->
template = templates[templateName]
opts.siteUrl = settings.siteUrl
opts.body = template.compiledTemplate(opts)
if settings.email?.templates?.customFooter?
opts.body += settings.email?.templates?.customFooter
return {
subject : template.subject(opts)
html: template.layout(opts)
text: template?.plainTextTemplate?(opts)
}

View File

@@ -1,20 +0,0 @@
settings = require("settings-sharelatex")
EmailBuilder = require "./EmailBuilder"
EmailSender = require "./EmailSender"
if !settings.email?
settings.email =
lifecycleEnabled:false
module.exports =
sendEmail : (emailType, opts, callback = (err)->)->
email = EmailBuilder.buildEmail emailType, opts
if email.type == "lifecycle" and !settings.email.lifecycle
return callback()
opts.html = email.html
opts.text = email.text
opts.subject = email.subject
EmailSender.sendEmail opts, (err)->
callback(err)

View File

@@ -1,79 +0,0 @@
logger = require('logger-sharelatex')
metrics = require('metrics-sharelatex')
Settings = require('settings-sharelatex')
nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport')
sgTransport = require('nodemailer-sendgrid-transport')
mandrillTransport = require('nodemailer-mandrill-transport')
rateLimiter = require('../../infrastructure/RateLimiter')
_ = require("underscore")
if Settings.email? and Settings.email.fromAddress?
defaultFromAddress = Settings.email.fromAddress
else
defaultFromAddress = ""
# provide dummy mailer unless we have a better one configured.
client =
sendMail: (options, callback = (err,status) ->) ->
logger.log options:options, "Would send email if enabled."
callback()
if Settings?.email?.parameters?.AWSAccessKeyID? or Settings?.email?.driver == 'ses'
logger.log "using aws ses for email"
nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters))
else if Settings?.email?.parameters?.sendgridApiKey?
logger.log "using sendgrid for email"
nm_client = nodemailer.createTransport(sgTransport({auth:{api_key:Settings?.email?.parameters?.sendgridApiKey}}))
else if Settings?.email?.parameters?.MandrillApiKey?
logger.log "using mandril for email"
nm_client = nodemailer.createTransport(mandrillTransport({auth:{apiKey:Settings?.email?.parameters?.MandrillApiKey}}))
else if Settings?.email?.parameters?
logger.log "using smtp for email"
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS")
nm_client = nodemailer.createTransport(smtp)
else
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
nm_client = client
if nm_client?
client = nm_client
else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
checkCanSendEmail = (options, callback)->
if !options.sendingUser_id? #email not sent from user, not rate limited
return callback(null, true)
opts =
endpointName: "send_email"
timeInterval: 60 * 60 * 3
subjectName: options.sendingUser_id
throttle: 100
rateLimiter.addCount opts, callback
module.exports =
sendEmail : (options, callback = (error) ->)->
logger.log receiver:options.to, subject:options.subject, "sending email"
checkCanSendEmail options, (err, canContinue)->
if err?
return callback(err)
if !canContinue
logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
return callback("rate limit hit sending email")
metrics.inc "email"
options =
to: options.to
from: defaultFromAddress
subject: options.subject
html: options.html
text: options.text
replyTo: options.replyTo || Settings.email.replyToAddress
socketTimeout: 30 * 1000
if Settings.email.textEncoding?
opts.textEncoding = textEncoding
client.sendMail options, (err, res)->
if err?
logger.err err:err, "error sending message"
err = new Error('Cannot send email')
else
logger.log "Message sent to #{options.to}"
callback(err)

View File

@@ -1,38 +0,0 @@
XRegExp = require('xregexp')
# A note about SAFE_REGEX:
# We have to escape the escape characters because XRegExp compiles it first.
# So it's equivalent to `^[\p{L}\p{N}\s\-_!&\(\)]+$]
# \p{L} = any letter in any language
# \p{N} = any kind of numeric character
# https://www.regular-expressions.info/unicode.html#prop is a good resource for
# more obscure regex features. standard RegExp does not support these
SAFE_REGEX = XRegExp("^[\\p{L}\\p{N}\\s\\-_!'&\\(\\)]+$")
EMAIL_REGEX = XRegExp("^[\\p{L}\\p{N}.+_-]+@[\\w.-]+$")
SpamSafe =
isSafeUserName: (name) ->
SAFE_REGEX.test(name) && name.length <= 30
isSafeProjectName: (name) ->
if XRegExp("\\p{Han}").test(name)
SAFE_REGEX.test(name) && name.length <= 30
SAFE_REGEX.test(name) && name.length <= 100
isSafeEmail: (email) ->
EMAIL_REGEX.test(email) && email.length <= 40
safeUserName: (name, alternative, project = false) ->
return name if SpamSafe.isSafeUserName name
alternative
safeProjectName: (name, alternative) ->
return name if SpamSafe.isSafeProjectName name
alternative
safeEmail: (email, alternative) ->
return email if SpamSafe.isSafeEmail email
alternative
module.exports = SpamSafe

View File

@@ -1,61 +0,0 @@
Errors = require "./Errors"
logger = require "logger-sharelatex"
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = ErrorController =
notFound: (req, res)->
res.status(404)
res.render 'general/404',
title: "page_not_found"
forbidden: (req, res) ->
res.status(403)
res.render 'user/restricted'
serverError: (req, res)->
res.status(500)
res.render 'general/500',
title: "Server Error"
accountMergeError: (req, res)->
res.status(500)
res.render 'general/account-merge-error',
title: "Account Access Error"
handleError: (error, req, res, next) ->
user = AuthenticationController.getSessionUser(req)
if error?.code is 'EBADCSRFTOKEN'
logger.warn err: error,url:req.url, method:req.method, user:user, "invalid csrf"
res.sendStatus(403)
return
if error instanceof Errors.NotFoundError
logger.warn {err: error, url: req.url}, "not found error"
ErrorController.notFound req, res
else if error instanceof Errors.ForbiddenError
logger.error err: error, "forbidden error"
ErrorController.forbidden req, res
else if error instanceof Errors.TooManyRequestsError
logger.warn {err: error, url: req.url}, "too many requests error"
res.sendStatus(429)
else if error instanceof Errors.InvalidError
logger.warn {err: error, url: req.url}, "invalid error"
res.status(400)
res.send(error.message)
else if error instanceof Errors.InvalidNameError
logger.warn {err: error, url: req.url}, "invalid name error"
res.status(400)
res.send(error.message)
else if error instanceof Errors.AccountMergeError
logger.error err: error, "account merge error"
ErrorController.accountMergeError req, res
else
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middleware"
ErrorController.serverError req, res
handleApiError: (error, req, res, next) ->
if error instanceof Errors.NotFoundError
logger.warn {err: error, url: req.url}, "not found error"
res.sendStatus(404)
else
logger.error err: error, url:req.url, method:req.method, "error passed to top level next middleware"
res.sendStatus(500)

View File

@@ -1,147 +0,0 @@
NotFoundError = (message) ->
error = new Error(message)
error.name = "NotFoundError"
error.__proto__ = NotFoundError.prototype
return error
NotFoundError.prototype.__proto__ = Error.prototype
ForbiddenError = (message) ->
error = new Error(message)
error.name = "ForbiddenError"
error.__proto__ = ForbiddenError.prototype
return error
ForbiddenError.prototype.__proto__ = Error.prototype
ServiceNotConfiguredError = (message) ->
error = new Error(message)
error.name = "ServiceNotConfiguredError"
error.__proto__ = ServiceNotConfiguredError.prototype
return error
ServiceNotConfiguredError.prototype.__proto__ = Error.prototype
TooManyRequestsError = (message) ->
error = new Error(message)
error.name = "TooManyRequestsError"
error.__proto__ = TooManyRequestsError.prototype
return error
TooManyRequestsError.prototype.__proto__ = Error.prototype
InvalidNameError = (message) ->
error = new Error(message)
error.name = "InvalidNameError"
error.__proto__ = InvalidNameError.prototype
return error
InvalidNameError.prototype.__proto__ = Error.prototype
UnsupportedFileTypeError = (message) ->
error = new Error(message)
error.name = "UnsupportedFileTypeError"
error.__proto__ = UnsupportedFileTypeError.prototype
return error
UnsupportedFileTypeError.prototype.__proto__ = Error.prototype
UnsupportedExportRecordsError = (message) ->
error = new Error(message)
error.name = "UnsupportedExportRecordsError"
error.__proto__ = UnsupportedExportRecordsError.prototype
return error
UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype
V1HistoryNotSyncedError = (message) ->
error = new Error(message)
error.name = "V1HistoryNotSyncedError"
error.__proto__ = V1HistoryNotSyncedError.prototype
return error
V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype
ProjectHistoryDisabledError = (message) ->
error = new Error(message)
error.name = "ProjectHistoryDisabledError"
error.__proto__ = ProjectHistoryDisabledError.prototype
return error
ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype
V1ConnectionError = (message) ->
error = new Error(message)
error.name = "V1ConnectionError"
error.__proto__ = V1ConnectionError.prototype
return error
V1ConnectionError.prototype.__proto__ = Error.prototype
UnconfirmedEmailError = (message) ->
error = new Error(message)
error.name = "UnconfirmedEmailError"
error.__proto__ = UnconfirmedEmailError.prototype
return error
UnconfirmedEmailError.prototype.__proto__ = Error.prototype
EmailExistsError = (message) ->
error = new Error(message)
error.name = "EmailExistsError"
error.__proto__ = EmailExistsError.prototype
return error
EmailExistsError.prototype.__proto__ = Error.prototype
InvalidError = (message) ->
error = new Error(message)
error.name = "InvalidError"
error.__proto__ = InvalidError.prototype
return error
InvalidError.prototype.__proto__ = Error.prototype
AccountMergeError = (message) ->
error = new Error(message)
error.name = "AccountMergeError"
error.__proto__ = AccountMergeError.prototype
return error
AccountMergeError.prototype.__proto__ = Error.prototype
NotInV2Error = (message) ->
error = new Error(message)
error.name = "NotInV2Error"
error.__proto__ = NotInV2Error.prototype
return error
NotInV2Error.prototype.__proto__ = Error.prototype
SLInV2Error = (message) ->
error = new Error(message)
error.name = "SLInV2Error"
error.__proto__ = SLInV2Error.prototype
return error
SLInV2Error.prototype.__proto__ = Error.prototype
ThirdPartyUserNotFoundError = (message) ->
message = "user not found for provider and external id" unless message?
error = new Error(message)
error.name = "ThirdPartyUserNotFoundError"
error.__proto__ = SLInV2Error.prototype
return error
ThirdPartyUserNotFoundError.prototype.__proto__ = Error.prototype
SubscriptionAdminDeletionError = (message) ->
message = "subscription admins cannot be deleted" unless message?
error = new Error(message)
error.name = "SubscriptionAdminDeletionError"
error.__proto__ = SubscriptionAdminDeletionError.prototype
return error
SubscriptionAdminDeletionError.prototype.__proto__ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
ForbiddenError: ForbiddenError
ServiceNotConfiguredError: ServiceNotConfiguredError
TooManyRequestsError: TooManyRequestsError
InvalidNameError: InvalidNameError
UnsupportedFileTypeError: UnsupportedFileTypeError
UnsupportedExportRecordsError: UnsupportedExportRecordsError
V1HistoryNotSyncedError: V1HistoryNotSyncedError
ProjectHistoryDisabledError: ProjectHistoryDisabledError
V1ConnectionError: V1ConnectionError
UnconfirmedEmailError: UnconfirmedEmailError
EmailExistsError: EmailExistsError
InvalidError: InvalidError
AccountMergeError: AccountMergeError
NotInV2Error: NotInV2Error
SLInV2Error: SLInV2Error
ThirdPartyUserNotFoundError: ThirdPartyUserNotFoundError
SubscriptionAdminDeletionError: SubscriptionAdminDeletionError

View File

@@ -1,72 +0,0 @@
ExportsHandler = require("./ExportsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
module.exports =
exportProject: (req, res, next) ->
{project_id, brand_variation_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
export_params = {
project_id: project_id,
brand_variation_id: brand_variation_id,
user_id: user_id
}
if req.body
export_params.first_name = req.body.firstName.trim() if req.body.firstName
export_params.last_name = req.body.lastName.trim() if req.body.lastName
# additional parameters for gallery exports
export_params.title = req.body.title.trim() if req.body.title
export_params.description = req.body.description.trim() if req.body.description
export_params.author = req.body.author.trim() if req.body.author
export_params.license = req.body.license.trim() if req.body.license
export_params.show_source = req.body.showSource if req.body.showSource?
ExportsHandler.exportProject export_params, (err, export_data) ->
if err?
if err.forwardResponse?
logger.log {responseError: err.forwardResponse}, "forwarding response"
statusCode = err.forwardResponse.status || 500
return res.status(statusCode).json err.forwardResponse
else
return next(err)
logger.log
user_id:user_id
project_id: project_id
brand_variation_id:brand_variation_id
export_v1_id:export_data.v1_id
"exported project"
res.json export_v1_id: export_data.v1_id
exportStatus: (req, res) ->
{export_id} = req.params
ExportsHandler.fetchExport export_id, (err, export_json) ->
if err?
json = {
status_summary: 'failed',
status_detail: err.toString,
}
res.json export_json: json
return err
parsed_export = JSON.parse(export_json)
json = {
status_summary: parsed_export.status_summary,
status_detail: parsed_export.status_detail,
partner_submission_id: parsed_export.partner_submission_id,
v2_user_email: parsed_export.v2_user_email,
v2_user_first_name: parsed_export.v2_user_first_name,
v2_user_last_name: parsed_export.v2_user_last_name,
title: parsed_export.title,
token: parsed_export.token
}
res.json export_json: json
exportDownload: (req, res, next) ->
{type, export_id} = req.params
AuthenticationController.getLoggedInUserId(req)
ExportsHandler.fetchDownload export_id, type, (err, export_file_url) ->
return next(err) if err?
res.redirect export_file_url

View File

@@ -1,145 +0,0 @@
ProjectGetter = require('../Project/ProjectGetter')
ProjectHistoryHandler = require('../Project/ProjectHistoryHandler')
ProjectLocator = require('../Project/ProjectLocator')
ProjectRootDocManager = require('../Project/ProjectRootDocManager')
UserGetter = require('../User/UserGetter')
logger = require('logger-sharelatex')
settings = require 'settings-sharelatex'
async = require 'async'
request = require 'request'
request = request.defaults()
settings = require 'settings-sharelatex'
module.exports = ExportsHandler = self =
exportProject: (export_params, callback=(error, export_data) ->) ->
self._buildExport export_params, (err, export_data) ->
return callback(err) if err?
self._requestExport export_data, (err, export_v1_id) ->
return callback(err) if err?
export_data.v1_id = export_v1_id
# TODO: possibly store the export data in Mongo
callback null, export_data
_buildExport: (export_params, callback=(err, export_data) ->) ->
{project_id, user_id, brand_variation_id, title, description, author,
license, show_source} = export_params
jobs =
project: (cb) ->
ProjectGetter.getProject project_id, cb
# TODO: when we update async, signature will change from (cb, results) to (results, cb)
rootDoc: [ 'project', (cb, results) ->
ProjectRootDocManager.ensureRootDocumentIsValid project_id, (error) ->
return callback(error) if error?
ProjectLocator.findRootDoc {project: results.project, project_id: project_id}, cb
]
user: (cb) ->
UserGetter.getUser user_id, {first_name: 1, last_name: 1, email: 1, overleaf: 1}, cb
historyVersion: (cb) ->
ProjectHistoryHandler.ensureHistoryExistsForProject project_id, (error) ->
return callback(error) if error?
self._requestVersion project_id, cb
async.auto jobs, (err, results) ->
if err?
logger.err err:err, project_id:project_id, user_id:user_id, brand_variation_id:brand_variation_id, "error building project export"
return callback(err)
{project, rootDoc, user, historyVersion} = results
if !rootDoc[1]?
err = new Error("cannot export project without root doc")
logger.err err:err, project_id: project_id
return callback(err)
if export_params.first_name && export_params.last_name
user.first_name = export_params.first_name
user.last_name = export_params.last_name
export_data =
project:
id: project_id
rootDocPath: rootDoc[1]?.fileSystem
historyId: project.overleaf?.history?.id
historyVersion: historyVersion
v1ProjectId: project.overleaf?.id
metadata:
compiler: project.compiler
imageName: project.imageName
title: title
description: description
author: author
license: license
showSource: show_source
user:
id: user_id
firstName: user.first_name
lastName: user.last_name
email: user.email
orcidId: null # until v2 gets ORCID
v1UserId: user.overleaf?.id
destination:
brandVariationId: brand_variation_id
options:
callbackUrl: null # for now, until we want v1 to call us back
callback null, export_data
_requestExport: (export_data, callback=(err, export_v1_id) ->) ->
request.post {
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports"
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
json: export_data
}, (err, res, body) ->
if err?
logger.err err:err, export:export_data, "error making request to v1 export"
callback err
else if 200 <= res.statusCode < 300
callback null, body.exportId
else
logger.err export:export_data, "v1 export returned failure; forwarding: #{body}"
# pass the v1 error along for the publish modal to handle
callback {forwardResponse: body}
_requestVersion: (project_id, callback=(err, export_v1_id) ->) ->
request.get {
url: "#{settings.apis.project_history.url}/project/#{project_id}/version"
json: true
}, (err, res, body) ->
if err?
logger.err err:err, project_id:project_id, "error making request to project history"
callback err
else if res.statusCode >= 200 and res.statusCode < 300
callback null, body.version
else
err = new Error("project history version returned a failure status code: #{res.statusCode}")
logger.err err:err, project_id:project_id, "project history version returned failure status code: #{res.statusCode}"
callback err
fetchExport: (export_id, callback=(err, export_json) ->) ->
request.get {
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}"
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
}, (err, res, body) ->
if err?
logger.err err:err, export:export_id, "error making request to v1 export"
callback err
else if 200 <= res.statusCode < 300
callback null, body
else
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
logger.err err:err, export:export_id, "v1 export returned failure status code: #{res.statusCode}"
callback err
fetchDownload: (export_id, type, callback=(err, file_url) ->) ->
request.get {
url: "#{settings.apis.v1.url}/api/v1/sharelatex/exports/#{export_id}/#{type}_url"
auth: {user: settings.apis.v1.user, pass: settings.apis.v1.pass }
}, (err, res, body) ->
if err?
logger.err err:err, export:export_id, "error making request to v1 export"
callback err
else if 200 <= res.statusCode < 300
callback null, body
else
err = new Error("v1 export returned a failure status code: #{res.statusCode}")
logger.err err:err, export:export_id, "v1 export zip fetch returned failure status code: #{res.statusCode}"
callback err

View File

@@ -1,35 +0,0 @@
crypto = require "crypto"
logger = require("logger-sharelatex")
fs = require("fs")
_ = require("underscore")
module.exports = FileHashManager =
computeHash: (filePath, callback = (error, hashValue) ->) ->
callback = _.once(callback) # avoid double callbacks
# taken from v1/history/storage/lib/blob_hash.js
getGitBlobHeader = (byteLength) ->
return 'blob ' + byteLength + '\x00'
getByteLengthOfFile = (cb) ->
fs.stat filePath, (err, stats) ->
return cb(err) if err?
cb(null, stats.size)
getByteLengthOfFile (err, byteLength) ->
return callback(err) if err?
input = fs.createReadStream(filePath)
input.on 'error', (err) ->
logger.err {filePath: filePath, err:err}, "error opening file in computeHash"
return callback(err)
hash = crypto.createHash("sha1")
hash.setEncoding('hex')
hash.update(getGitBlobHeader(byteLength))
hash.on 'readable', () ->
result = hash.read()
if result?
callback(null, result.toString('hex'))
input.pipe(hash)

View File

@@ -1,39 +0,0 @@
logger = require('logger-sharelatex')
FileStoreHandler = require("./FileStoreHandler")
ProjectLocator = require("../Project/ProjectLocator")
_ = require('underscore')
is_mobile_safari = (user_agent) ->
user_agent and (user_agent.indexOf('iPhone') >= 0 or
user_agent.indexOf('iPad') >= 0)
is_html = (file) ->
ends_with = (ext) ->
file.name? and
file.name.length > ext.length and
(file.name.lastIndexOf(ext) == file.name.length - ext.length)
ends_with('.html') or ends_with('.htm') or ends_with('.xhtml')
module.exports =
getFile : (req, res)->
project_id = req.params.Project_id
file_id = req.params.File_id
queryString = req.query
user_agent = req.get('User-Agent')
logger.log project_id: project_id, file_id: file_id, queryString:queryString, "file download"
ProjectLocator.findElement {project_id: project_id, element_id: file_id, type: "file"}, (err, file)->
if err?
logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error finding element for downloading file"
return res.sendStatus 500
FileStoreHandler.getFileStream project_id, file_id, queryString, (err, stream)->
if err?
logger.err err:err, project_id: project_id, file_id: file_id, queryString:queryString, "error getting file stream for downloading file"
return res.sendStatus 500
# mobile safari will try to render html files, prevent this
if (is_mobile_safari(user_agent) and is_html(file))
logger.log filename: file.name, user_agent: user_agent, "sending html file to mobile-safari as plain text"
res.setHeader('Content-Type', 'text/plain')
res.setContentDisposition('attachment', {filename: file.name})
stream.pipe res

View File

@@ -1,124 +0,0 @@
logger = require("logger-sharelatex")
fs = require("fs")
request = require("request")
settings = require("settings-sharelatex")
Async = require('async')
FileHashManager = require("./FileHashManager")
File = require('../../models/File').File
oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5
module.exports = FileStoreHandler =
RETRY_ATTEMPTS: 3
uploadFileFromDisk: (project_id, file_args, fsPath, callback = (error, url, fileRef) ->)->
fs.lstat fsPath, (err, stat)->
if err?
logger.err err:err, project_id:project_id, file_args:file_args, fsPath:fsPath, "error stating file"
callback(err)
if !stat?
logger.err project_id:project_id, file_args:file_args, fsPath:fsPath, "stat is not available, can not check file from disk"
return callback(new Error("error getting stat, not available"))
if !stat.isFile()
logger.log project_id:project_id, file_args:file_args, fsPath:fsPath, "tried to upload symlink, not contining"
return callback(new Error("can not upload symlink"))
Async.retry FileStoreHandler.RETRY_ATTEMPTS, (cb, results) ->
FileStoreHandler._doUploadFileFromDisk project_id, file_args, fsPath, cb
, (err, result) ->
if err?
logger.err {err, project_id, file_args}, "Error uploading file, retries failed"
return callback(err)
callback(err, result.url, result.fileRef)
_doUploadFileFromDisk: (project_id, file_args, fsPath, callback = (err, result)->) ->
_cb = callback
callback = (err, result...) ->
callback = -> # avoid double callbacks
_cb(err, result...)
FileHashManager.computeHash fsPath, (err, hashValue) ->
return callback(err) if err?
fileRef = new File(Object.assign({}, file_args, {hash: hashValue}))
file_id = fileRef._id
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, hash: hashValue, fileRef:fileRef, "uploading file from disk"
readStream = fs.createReadStream(fsPath)
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
readStream.on "open", () ->
url = FileStoreHandler._buildUrl(project_id, file_id)
opts =
method: "post"
uri: url
timeout:fiveMinsInMs
headers:
"X-File-Hash-From-Web": hashValue # send the hash to the filestore as a custom header so it can be checked
writeStream = request(opts)
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
writeStream.on 'response', (response) ->
if response.statusCode not in [200, 201]
err = new Error("non-ok response from filestore for upload: #{response.statusCode}")
logger.err {err, statusCode: response.statusCode}, "error uploading to filestore"
callback(err)
else
callback(null, {url, fileRef}) # have to pass back an object because async.retry only accepts a single result argument
readStream.pipe writeStream
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}"
timeout:fiveMinsInMs
headers: {}
if query? and query['range']?
rangeText = query['range']
if rangeText && rangeText.match? && rangeText.match(/\d+-\d+/)
opts.headers['range'] = "bytes=#{query['range']}"
readStream = request(opts)
readStream.on "error", (err) ->
logger.err {err, project_id, file_id, query, opts}, "error in file stream"
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)
timeout:fiveMinsInMs
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)
timeout:fiveMinsInMs
request opts, (err, response)->
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)
else if 200 <= response.statusCode < 300
# successful response
callback(null, opts.uri)
else
err = new Error("non-ok response from filestore for copyFile: #{response.statusCode}")
logger.err {uri: opts.uri, statusCode: response.statusCode}, "error uploading to filestore"
callback(err)
_buildUrl: (project_id, file_id)->
return "#{settings.apis.filestore.url}/project/#{project_id}/file/#{file_id}"

View File

@@ -1,87 +0,0 @@
Mocha = require "mocha"
Base = require("mocha/lib/reporters/base")
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("health_check")
settings = require("settings-sharelatex")
logger = require "logger-sharelatex"
domain = require "domain"
UserGetter = require("../User/UserGetter")
module.exports = HealthCheckController =
check: (req, res, next = (error) ->) ->
d = domain.create()
d.on "error", (error) ->
logger.err err: error, "error in mocha"
d.run () ->
mocha = new Mocha(reporter: Reporter(res), timeout: 10000)
mocha.addFile("test/smoke/js/SmokeTests.js")
mocha.run () ->
# TODO: combine this with the smoke-test-sharelatex module
# we need to clean up all references to the smokeTest module
# so it can be garbage collected. The only reference should
# be in its parent, when it is loaded by mocha.addFile.
path = require.resolve(__dirname + "/../../../../test/smoke/js/SmokeTests.js")
smokeTestModule = require.cache[path]
if smokeTestModule?
parent = smokeTestModule.parent
while (idx = parent.children.indexOf(smokeTestModule)) != -1
parent.children.splice(idx, 1)
else
logger.warn {path}, "smokeTestModule not defined"
# remove the smokeTest from the module cache
delete require.cache[path]
checkRedis: (req, res, next)->
rclient.healthCheck (error) ->
if error?
logger.err {err: error}, "failed redis health check"
res.sendStatus 500
else
res.sendStatus 200
checkMongo: (req, res, next)->
logger.log "running mongo health check"
UserGetter.getUserEmail settings.smokeTest.userId, (err, email)->
if err?
logger.err err:err, "mongo health check failed, error present"
return res.sendStatus 500
else if !email?
logger.err err:err, "mongo health check failed, no emai present in find result"
return res.sendStatus 500
else
logger.log email:email, "mongo health check passed"
res.sendStatus 200
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
logger.err failures:failures, "health check failed"
res.status(500).send(JSON.stringify(results, null, 2))
else
res.status(200).send(JSON.stringify(results, null, 2))

View File

@@ -1,13 +0,0 @@
EMAIL_REGEXP = /^(([^<>()[\]\\.,;:\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,}))$/
module.exports = EmailHelper =
parseEmail: (email) ->
return null unless email?
return null if email.length > 254
email = email.trim().toLowerCase()
matched = email.match EMAIL_REGEXP
return null unless matched? && matched[0]?
matched[0]

View File

@@ -1,18 +0,0 @@
JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g
JSON_ESCAPE =
'&': '\\u0026'
'>': '\\u003e'
'<': '\\u003c'
'\u2028': '\\u2028'
'\u2029': '\\u2029'
module.exports = StringHelper =
# stringifies and escapes a json object for use in a script. This ensures that &, < and > characters are escaped,
# along with quotes. This ensures that the string can be safely rendered into HTML. See rationale at:
# https://api.rubyonrails.org/classes/ERB/Util.html#method-c-json_escape
# and implementation lifted from:
# https://github.com/ember-fastboot/fastboot/blob/cafd96c48564d8384eb83dc908303dba8ece10fd/src/ember-app.js#L496-L510
stringifyJsonForScript: (object) ->
return JSON.stringify(object).replace JSON_ESCAPE_REGEXP, (match) ->
return JSON_ESCAPE[match]

View File

@@ -1,14 +0,0 @@
Settings = require 'settings-sharelatex'
module.exports = UrlHelper =
wrapUrlWithProxy: (url) ->
# TODO: Consider what to do for Community and Enterprise edition?
if !Settings.apis?.linkedUrlProxy?.url?
throw new Error('no linked url proxy configured')
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
prependHttpIfNeeded: (url) ->
if !url.match('://')
url = 'http://' + url
return url

View File

@@ -1,209 +0,0 @@
_ = require "lodash"
async = require "async"
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
Errors = require "../Errors/Errors"
HistoryManager = require "./HistoryManager"
ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
ProjectEntityUpdateHandler = require "../Project/ProjectEntityUpdateHandler"
RestoreManager = require "./RestoreManager"
module.exports = HistoryController =
selectHistoryApi: (req, res, next = (error) ->) ->
project_id = req.params?.Project_id
# find out which type of history service this project uses
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
history = project.overleaf?.history
if history?.id? and history?.display
req.useProjectHistory = true
else
req.useProjectHistory = false
next()
ensureProjectHistoryEnabled: (req, res, next = (error) ->) ->
if req.useProjectHistory?
next()
else
logger.log {project_id}, "project history not enabled"
res.sendStatus(404)
proxyToHistoryApi: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
logger.log url: url, "proxying to history api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error url: url, err: error, "history API error"
next(error)
proxyToHistoryApiAndInjectUserDetails: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url
logger.log url: url, "proxying to history api"
HistoryController._makeRequest {
url: url
method: req.method
json: true
headers:
"X-User-Id": user_id
}, (error, body) ->
return next(error) if error?
HistoryManager.injectUserDetails body, (error, data) ->
return next(error) if error?
res.json data
buildHistoryServiceUrl: (useProjectHistory) ->
# choose a history service, either document-level (trackchanges)
# or project-level (project_history)
if useProjectHistory
return settings.apis.project_history.url
else
return settings.apis.trackchanges.url
resyncProjectHistory: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
ProjectEntityUpdateHandler.resyncProjectHistory project_id, (error) ->
return res.sendStatus(404) if error instanceof Errors.ProjectHistoryDisabledError
return next(error) if error?
res.sendStatus 204
restoreFileFromV2: (req, res, next) ->
{project_id} = req.params
{version, pathname} = req.body
user_id = AuthenticationController.getLoggedInUserId req
logger.log {project_id, version, pathname}, "restoring file from v2"
RestoreManager.restoreFileFromV2 user_id, project_id, version, pathname, (error, entity) ->
return next(error) if error?
res.json {
type: entity.type,
id: entity._id
}
restoreDocFromDeletedDoc: (req, res, next) ->
{project_id, doc_id} = req.params
{name} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
if !name?
return res.sendStatus 400 # Malformed request
logger.log {project_id, doc_id, user_id}, "restoring doc from v1 deleted doc"
RestoreManager.restoreDocFromDeletedDoc user_id, project_id, doc_id, name, (err, doc) =>
return next(error) if error?
res.json {
doc_id: doc._id
}
getLabels: (req, res, next) ->
project_id = req.params.Project_id
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "GET"
url: "#{settings.apis.project_history.url}/project/#{project_id}/labels"
json: true
}, (error, labels) ->
return next(error) if error?
res.json labels
createLabel: (req, res, next) ->
project_id = req.params.Project_id
{comment, version} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "POST"
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels"
json: {comment, version}
}, (error, label) ->
return next(error) if error?
res.json label
deleteLabel: (req, res, next) ->
project_id = req.params.Project_id
label_id = req.params.label_id
user_id = AuthenticationController.getLoggedInUserId(req)
HistoryController._makeRequest {
method: "DELETE"
url: "#{settings.apis.project_history.url}/project/#{project_id}/user/#{user_id}/labels/#{label_id}"
}, (error) ->
return next(error) if error?
res.sendStatus 204
_makeRequest: (options, callback) ->
request options, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300
callback(null, body)
else
error = new Error("history api responded with non-success code: #{response.statusCode}")
logger.error err: error, "project-history api responded with non-success code: #{response.statusCode}"
callback(error)
downloadZipOfVersion: (req, res, next) ->
{project_id, version} = req.params
logger.log {project_id, version}, "got request for zip file at version"
ProjectDetailsHandler.getDetails project_id, (err, project) ->
return next(err) if err?
v1_id = project.overleaf?.history?.id
if !v1_id?
logger.err {project_id, version}, 'got request for zip version of non-v1 history project'
return res.sendStatus(402)
HistoryController._pipeHistoryZipToResponse v1_id, version, "#{project.name} (Version #{version})", res, next
_pipeHistoryZipToResponse: (v1_project_id, version, name, res, next) ->
# increase timeout to 6 minutes
res.setTimeout(6 * 60 * 1000)
url = "#{settings.apis.v1_history.url}/projects/#{v1_project_id}/version/#{version}/zip"
logger.log {v1_project_id, version, url}, "getting s3 url from history api"
options =
auth:
user: settings.apis.v1_history.user
pass: settings.apis.v1_history.pass
json: true
method: 'post'
url: url
request options, (err, response, body) ->
if err
logger.error {err, v1_project_id, version}, "history API error"
return next(err)
retryAttempt = 0
retryDelay = 2000
# retry for about 6 minutes starting with short delay
async.retry 40,
(callback) ->
setTimeout(() ->
# increase delay by 1 second up to 10
retryDelay += 1000 if retryDelay < 10000
retryAttempt++
getReq = request(
url: body.zipUrl
sendImmediately: true
)
getReq.on 'response', (response) ->
return callback new Error "invalid response" unless response.statusCode == 200
# pipe also proxies the headers, but we want to customize these ones
delete response.headers['content-disposition']
delete response.headers['content-type']
res.status response.statusCode
res.setContentDisposition(
'attachment',
{filename: "#{name}.zip"}
)
res.contentType('application/zip')
getReq.pipe(res)
callback()
getReq.on "error", (err) ->
logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download error"
callback(err)
, retryDelay)
(err) ->
if err
logger.error {err, v1_project_id, version, retryAttempt}, "history s3 download failed"
next(err)

View File

@@ -1,99 +0,0 @@
request = require "request"
settings = require "settings-sharelatex"
async = require 'async'
UserGetter = require "../User/UserGetter"
module.exports = HistoryManager =
initializeProject: (callback = (error, history_id) ->) ->
return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
request.post {
url: "#{settings.apis.project_history.url}/project"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
try
project = JSON.parse(body)
catch error
return callback(error)
overleaf_id = project?.project?.id
if !overleaf_id
error = new Error("project-history did not provide an id", project)
return callback(error)
callback null, { overleaf_id }
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
flushProject: (project_id, callback = (error) ->) ->
request.post {
url: "#{settings.apis.project_history.url}/project/#{project_id}/flush"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
callback()
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
resyncProject: (project_id, callback = (error) ->) ->
request.post {
url: "#{settings.apis.project_history.url}/project/#{project_id}/resync"
}, (error, res, body)->
return callback(error) if error?
if res.statusCode >= 200 and res.statusCode < 300
callback()
else
error = new Error("project-history returned a non-success status code: #{res.statusCode}")
callback error
injectUserDetails: (data, callback = (error, data_with_users) ->) ->
# data can be either:
# {
# diff: [{
# i: "foo",
# meta: {
# users: ["user_id", { first_name: "James", ... }, ...]
# ...
# }
# }, ...]
# }
# or
# {
# updates: [{
# pathnames: ["main.tex"]
# meta: {
# users: ["user_id", { first_name: "James", ... }, ...]
# ...
# },
# ...
# }, ...]
# }
# Either way, the top level key points to an array of objects with a meta.users property
# that we need to replace user_ids with populated user objects.
# Note that some entries in the users arrays may already have user objects from the v1 history
# service
user_ids = new Set()
for entry in data.diff or data.updates or []
for user in entry.meta?.users or []
if typeof user == "string"
user_ids.add user
user_ids = Array.from(user_ids)
UserGetter.getUsers user_ids, { first_name: 1, last_name: 1, email: 1 }, (error, users_array) ->
return callback(error) if error?
users = {}
for user in users_array or []
users[user._id.toString()] = HistoryManager._userView(user)
for entry in data.diff or data.updates or []
entry.meta?.users = (entry.meta?.users or []).map (user) ->
if typeof user == "string"
return users[user]
else
return user
callback null, data
_userView: (user) ->
{ _id, first_name, last_name, email } = user
return { first_name, last_name, email, id: _id }

View File

@@ -1,60 +0,0 @@
Settings = require 'settings-sharelatex'
Path = require 'path'
FileWriter = require '../../infrastructure/FileWriter'
FileSystemImportManager = require '../Uploads/FileSystemImportManager'
ProjectEntityHandler = require '../Project/ProjectEntityHandler'
ProjectLocator = require '../Project/ProjectLocator'
EditorController = require '../Editor/EditorController'
Errors = require '../Errors/Errors'
moment = require 'moment'
module.exports = RestoreManager =
restoreDocFromDeletedDoc: (user_id, project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
# This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system.
# It looks up the deleted doc's contents, and then creates a new doc with the same content.
# We don't actually remove the deleted doc entry, just create a new one from its lines.
ProjectEntityHandler.getDoc project_id, doc_id, include_deleted: true, (error, lines) ->
return callback(error) if error?
addDocWithName = (name, callback) ->
EditorController.addDoc project_id, null, name, lines, 'restore', user_id, callback
RestoreManager._addEntityWithUniqueName addDocWithName, name, callback
restoreFileFromV2: (user_id, project_id, version, pathname, callback = (error, entity) ->) ->
RestoreManager._writeFileVersionToDisk project_id, version, pathname, (error, fsPath) ->
return callback(error) if error?
basename = Path.basename(pathname)
dirname = Path.dirname(pathname)
if dirname == '.' # no directory
dirname = ''
RestoreManager._findOrCreateFolder project_id, dirname, (error, parent_folder_id) ->
return callback(error) if error?
addEntityWithName = (name, callback) ->
FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, name, fsPath, false, callback
RestoreManager._addEntityWithUniqueName addEntityWithName, basename, callback
_findOrCreateFolder: (project_id, dirname, callback = (error, folder_id) ->) ->
EditorController.mkdirp project_id, dirname, (error, newFolders, lastFolder) ->
return callback(error) if error?
return callback(null, lastFolder?._id)
_addEntityWithUniqueName: (addEntityWithName, basename, callback = (error) ->) ->
addEntityWithName basename, (error, entity) ->
if error?
if error instanceof Errors.InvalidNameError
# likely a duplicate name, so try with a prefix
date = moment(new Date()).format('Do MMM YY H:mm:ss')
# Move extension to the end so the file type is preserved
extension = Path.extname(basename)
basename = Path.basename(basename, extension)
basename = "#{basename} (Restored on #{date})"
if extension != ''
basename = "#{basename}#{extension}"
addEntityWithName basename, callback
else
callback(error)
else
callback(null, entity)
_writeFileVersionToDisk: (project_id, version, pathname, callback = (error, fsPath) ->) ->
url = "#{Settings.apis.project_history.url}/project/#{project_id}/version/#{version}/#{encodeURIComponent(pathname)}"
FileWriter.writeUrlToDisk project_id, url, callback

View File

@@ -1,25 +0,0 @@
InactiveProjectManager = require("./InactiveProjectManager")
logger = require("logger-sharelatex")
module.exports =
deactivateOldProjects: (req, res)->
logger.log "recived request to deactivate old projects"
numberOfProjectsToArchive = parseInt(req.body.numberOfProjectsToArchive, 10)
ageOfProjects = req.body.ageOfProjects
InactiveProjectManager.deactivateOldProjects numberOfProjectsToArchive, ageOfProjects, (err, projectsDeactivated)->
if err?
res.sendStatus(500)
else
res.send(projectsDeactivated)
deactivateProject: (req, res)->
project_id = req.params.project_id
logger.log project_id:project_id, "recived request to deactivating project"
InactiveProjectManager.deactivateProject project_id, (err)->
if err?
res.sendStatus 500
else
res.sendStatus 200

View File

@@ -1,59 +0,0 @@
async = require("async")
_ = require("underscore")
logger = require("logger-sharelatex")
DocstoreManager = require("../Docstore/DocstoreManager")
ProjectGetter = require("../Project/ProjectGetter")
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
Project = require("../../models/Project").Project
MILISECONDS_IN_DAY = 86400000
module.exports = InactiveProjectManager =
reactivateProjectIfRequired: (project_id, callback)->
ProjectGetter.getProject project_id, {active:true}, (err, project)->
if err?
logger.err err:err, project_id:project_id, "error getting project"
return callback(err)
logger.log project_id:project_id, active:project.active, "seeing if need to reactivate project"
if project.active
return callback()
DocstoreManager.unarchiveProject project_id, (err)->
if err?
logger.err err:err, project_id:project_id, "error reactivating project in docstore"
return callback(err)
ProjectUpdateHandler.markAsActive project_id, callback
deactivateOldProjects: (limit = 10, daysOld = 360, callback)->
oldProjectDate = new Date() - (MILISECONDS_IN_DAY * daysOld)
logger.log oldProjectDate:oldProjectDate, limit:limit, daysOld:daysOld, "starting process of deactivating old projects"
Project.find()
.where("lastOpened").lt(oldProjectDate)
.where("active").equals(true)
.select("_id")
.limit(limit)
.exec (err, projects)->
if err?
logger.err err:err, "could not get projects for deactivating"
jobs = _.map projects, (project)->
return (cb)->
InactiveProjectManager.deactivateProject project._id, cb
logger.log numberOfProjects:projects?.length, "deactivating projects"
async.series jobs, (err)->
if err?
logger.err err:err, "error deactivating projects"
callback err, projects
deactivateProject: (project_id, callback)->
logger.log project_id:project_id, "deactivating inactive project"
jobs = [
(cb)-> DocstoreManager.archiveProject project_id, cb
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
]
async.series jobs, (err)->
if err?
logger.err err:err, project_id:project_id, "error deactivating project"
callback(err)

View File

@@ -1,111 +0,0 @@
logger = require("logger-sharelatex")
metrics = require("metrics-sharelatex")
settings = require "settings-sharelatex"
request = require "request"
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = InstitutionsAPI =
getInstitutionAffiliations: (institutionId, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get institution affiliations"
}, (error, body) -> callback(error, body or [])
getInstitutionLicences: (institutionId, startDate, endDate, lag, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/institutions/#{institutionId.toString()}/institution_licences"
body: {start_date: startDate, end_date: endDate, lag}
defaultErrorMessage: "Couldn't get institution licences"
}, callback
getUserAffiliations: (userId, callback = (error, body) ->) ->
makeAffiliationRequest {
method: 'GET'
path: "/api/v2/users/#{userId.toString()}/affiliations"
defaultErrorMessage: "Couldn't get user affiliations"
}, (error, body) -> callback(error, body or [])
addAffiliation: (userId, email, affiliationOptions, callback) ->
unless callback? # affiliationOptions is optional
callback = affiliationOptions
affiliationOptions = {}
{ university, department, role, confirmedAt } = affiliationOptions
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations"
body: { email, university, department, role, confirmedAt }
defaultErrorMessage: "Couldn't create affiliation"
}, (error, body) ->
if error
return callback(error, body)
# have notifications delete any ip matcher notifications for this university
logger.log university
NotificationsBuilder.ipMatcherAffiliation(userId).read university?.id, (err) ->
if err
logger.err err:err, "Something went wrong marking ip notifications read"
callback(error, body)
removeAffiliation: (userId, email, callback = (error) ->) ->
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations/remove"
body: { email }
extraSuccessStatusCodes: [404] # `Not Found` responses are considered successful
defaultErrorMessage: "Couldn't remove affiliation"
}, callback
endorseAffiliation: (userId, email, role, department, callback = (error) ->) ->
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations/endorse"
body: { email, role, department }
defaultErrorMessage: "Couldn't endorse affiliation"
}, callback
deleteAffiliations: (userId, callback = (error) ->) ->
makeAffiliationRequest {
method: 'DELETE'
path: "/api/v2/users/#{userId.toString()}/affiliations"
defaultErrorMessage: "Couldn't delete affiliations"
}, callback
makeAffiliationRequest = (requestOptions, callback = (error) ->) ->
return callback(null) unless settings?.apis?.v1?.url # service is not configured
requestOptions.extraSuccessStatusCodes ||= []
request {
method: requestOptions.method
url: "#{settings.apis.v1.url}#{requestOptions.path}"
body: requestOptions.body
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
json: true,
timeout: 20 * 1000
}, (error, response, body) ->
return callback(error) if error?
isSuccess = 200 <= response.statusCode < 300
isSuccess ||= response.statusCode in requestOptions.extraSuccessStatusCodes
unless isSuccess
if body?.errors
errorMessage = "#{response.statusCode}: #{body.errors}"
else
errorMessage = "#{requestOptions.defaultErrorMessage}: #{response.statusCode}"
logger.err path: requestOptions.path, body: requestOptions.body, errorMessage
return callback(new Error(errorMessage))
callback(null, body)
[
'getInstitutionAffiliations'
'getUserAffiliations',
'addAffiliation',
'removeAffiliation',
].map (method) ->
metrics.timeAsyncMethod(
InstitutionsAPI, method, 'mongo.InstitutionsAPI', logger
)

View File

@@ -1,33 +0,0 @@
logger = require("logger-sharelatex")
UserGetter = require("../User/UserGetter")
{ addAffiliation } = require("../Institutions/InstitutionsAPI")
FeaturesUpdater = require('../Subscription/FeaturesUpdater')
async = require('async')
module.exports = InstitutionsController =
confirmDomain: (req, res, next) ->
hostname = req.body.hostname
affiliateUsers hostname, (error) ->
return next(error) if error?
res.sendStatus 200
affiliateUsers = (hostname, callback = (error)->) ->
reversedHostname = hostname.trim().split('').reverse().join('')
UserGetter.getUsersByHostname hostname, {_id:1, emails:1}, (error, users) ->
if error?
logger.err error: error, 'problem fetching users by hostname'
return callback(error)
async.map users, ((user, innerCallback) ->
affiliateUserByReversedHostname user, reversedHostname, innerCallback
), callback
affiliateUserByReversedHostname = (user, reversedHostname, callback) ->
matchingEmails = user.emails.filter (email) -> email.reversedHostname == reversedHostname
async.map matchingEmails, ((email, innerCallback) ->
addAffiliation user._id, email.email, {confirmedAt: email.confirmedAt}, (error) =>
if error?
logger.err error: error, 'problem adding affiliation while confirming hostname'
return innerCallback(error)
FeaturesUpdater.refreshFeatures user._id, true, innerCallback
), callback

View File

@@ -1,28 +0,0 @@
InstitutionsGetter = require './InstitutionsGetter'
PlansLocator = require '../Subscription/PlansLocator'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
module.exports = InstitutionsFeatures =
getInstitutionsFeatures: (userId, callback = (error, features) ->) ->
InstitutionsFeatures.getInstitutionsPlan userId, (error, plan) ->
return callback error if error?
plan = PlansLocator.findLocalPlanInSettings plan
callback(null, plan?.features or {})
getInstitutionsPlan: (userId, callback = (error, plan) ->) ->
InstitutionsFeatures.hasLicence userId, (error, hasLicence) ->
return callback error if error?
return callback(null, null) unless hasLicence
callback(null, Settings.institutionPlanCode)
hasLicence: (userId, callback = (error, hasLicence) ->) ->
InstitutionsGetter.getConfirmedInstitutions userId, (error, institutions) ->
return callback error if error?
hasLicence = institutions.some (institution) ->
institution.licence and institution.licence != 'free'
callback(null, hasLicence)

View File

@@ -1,19 +0,0 @@
UserGetter = require '../User/UserGetter'
UserMembershipsHandler = require "../UserMembership/UserMembershipsHandler"
UserMembershipEntityConfigs = require "../UserMembership/UserMembershipEntityConfigs"
logger = require 'logger-sharelatex'
module.exports = InstitutionsGetter =
getConfirmedInstitutions: (userId, callback = (error, institutions) ->) ->
UserGetter.getUserFullEmails userId, (error, emailsData) ->
return callback error if error?
confirmedInstitutions = emailsData.filter (emailData) ->
emailData.confirmedAt? and emailData.affiliation?.institution?.confirmed
.map (emailData) ->
emailData.affiliation?.institution
callback(null, confirmedInstitutions)
getManagedInstitutions: (user_id, callback = (error, managedInstitutions) ->) ->
UserMembershipsHandler.getEntitiesByUser UserMembershipEntityConfigs.institution, user_id, callback

View File

@@ -1,103 +0,0 @@
logger = require 'logger-sharelatex'
async = require 'async'
db = require("../../infrastructure/mongojs").db
_ = require("underscore")
ObjectId = require("../../infrastructure/mongojs").ObjectId
{ getInstitutionAffiliations } = require('./InstitutionsAPI')
FeaturesUpdater = require('../Subscription/FeaturesUpdater')
UserGetter = require('../User/UserGetter')
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
SubscriptionLocator = require("../Subscription/SubscriptionLocator")
Institution = require("../../models/Institution").Institution
Subscription = require("../../models/Subscription").Subscription
ASYNC_LIMIT = 10
module.exports = InstitutionsManager =
upgradeInstitutionUsers: (institutionId, callback = (error) ->) ->
async.waterfall [
(cb) ->
fetchInstitutionAndAffiliations institutionId, cb
(institution, affiliations, cb) ->
affiliations = _.map affiliations, (affiliation) ->
affiliation.institutionName = institution.name
affiliation.institutionId = institutionId
return affiliation
async.eachLimit affiliations, ASYNC_LIMIT, refreshFeatures, (err) -> cb(err)
], callback
checkInstitutionUsers: (institutionId, callback = (error) ->) ->
getInstitutionAffiliations institutionId, (error, affiliations) ->
UserGetter.getUsersByAnyConfirmedEmail(
affiliations.map((affiliation) -> affiliation.email),
{ features: 1 },
(error, users) ->
callback(error, checkFeatures(users))
)
getInstitutionUsersSubscriptions: (institutionId, callback = (error, subscriptions) ->) ->
getInstitutionAffiliations institutionId, (error, affiliations) ->
return callback(error) if error?
userIds = affiliations.map (affiliation) -> ObjectId(affiliation.user_id)
Subscription
.find admin_id: userIds, planCode: { $not: /trial/ }
.populate 'admin_id', 'email'
.exec callback
fetchInstitutionAndAffiliations = (institutionId, callback) ->
async.waterfall [
(cb) ->
Institution.findOne {v1Id: institutionId}, (err, institution) -> cb(err, institution)
(institution, cb) ->
institution.fetchV1Data (err, institution) -> cb(err, institution)
(institution, cb) ->
getInstitutionAffiliations institutionId, (err, affiliations) -> cb(err, institution, affiliations)
], callback
refreshFeatures = (affiliation, callback) ->
userId = ObjectId(affiliation.user_id)
async.waterfall [
(cb) ->
FeaturesUpdater.refreshFeatures userId, true, (err, features, featuresChanged) -> cb(err, featuresChanged)
(featuresChanged, cb) ->
getUserInfo userId, (error, user, subscription) -> cb(error, user, subscription, featuresChanged)
(user, subscription, featuresChanged, cb) ->
notifyUser user, affiliation, subscription, featuresChanged, cb
], callback
getUserInfo = (userId, callback) ->
async.waterfall [
(cb) ->
UserGetter.getUser userId, cb
(user, cb) ->
SubscriptionLocator.getUsersSubscription user, (err, subscription) -> cb(err, user, subscription)
], callback
notifyUser = (user, affiliation, subscription, featuresChanged, callback) ->
async.parallel [
(cb) ->
if featuresChanged
NotificationsBuilder.featuresUpgradedByAffiliation(affiliation, user).create cb
else
cb()
(cb) ->
if subscription? and !subscription.planCode.match(/(free|trial)/)? and !subscription.groupPlan
NotificationsBuilder.redundantPersonalSubscription(affiliation, user).create cb
else
cb()
], callback
checkFeatures = (users) ->
usersSummary = {
totalConfirmedUsers: users.length
totalConfirmedProUsers: 0
totalConfirmedNonProUsers: 0
confirmedNonProUsers: []
}
users.forEach((user) ->
if user.features.collaborators == -1 and user.features.trackChanges
usersSummary.totalConfirmedProUsers += 1
else
usersSummary.totalConfirmedNonProUsers += 1
usersSummary.confirmedNonProUsers.push user._id
)
return usersSummary

View File

@@ -1,149 +0,0 @@
AuthenticationController = require '../Authentication/AuthenticationController'
EditorController = require '../Editor/EditorController'
ProjectLocator = require '../Project/ProjectLocator'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
_ = require 'underscore'
LinkedFilesHandler = require './LinkedFilesHandler'
{
UrlFetchFailedError,
InvalidUrlError,
OutputFileFetchFailedError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
NotOriginalImporterError,
FeatureNotAvailableError,
RemoteServiceError,
FileCannotRefreshError
} = require './LinkedFilesErrors'
Modules = require '../../infrastructure/Modules'
module.exports = LinkedFilesController = {
Agents: _.extend(
{
url: require('./UrlAgent'),
project_file: require('./ProjectFileAgent'),
project_output_file: require('./ProjectOutputFileAgent'),
},
Modules.linkedFileAgentsIncludes()
)
_getAgent: (provider) ->
if !LinkedFilesController.Agents.hasOwnProperty(provider)
return null
unless provider in Settings.enabledLinkedFileTypes
return null
LinkedFilesController.Agents[provider]
createLinkedFile: (req, res, next) ->
{project_id} = req.params
{name, provider, data, parent_folder_id} = req.body
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, name, provider, data, parent_folder_id, user_id}, 'create linked file request'
Agent = LinkedFilesController._getAgent(provider)
if !Agent?
return res.sendStatus(400)
data.provider = provider
Agent.createLinkedFile project_id,
data,
name,
parent_folder_id,
user_id,
(err, newFileId) ->
return LinkedFilesController.handleError(err, req, res, next) if err?
res.json(new_file_id: newFileId)
refreshLinkedFile: (req, res, next) ->
{project_id, file_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, file_id, user_id}, 'refresh linked file request'
LinkedFilesHandler.getFileById project_id, file_id, (err, file, path, parentFolder) ->
return next(err) if err?
return res.sendStatus(404) if !file?
name = file.name
linkedFileData = file.linkedFileData
if !linkedFileData? || !linkedFileData?.provider?
return res.send(409)
provider = linkedFileData.provider
parent_folder_id = parentFolder._id
Agent = LinkedFilesController._getAgent(provider)
if !Agent?
return res.sendStatus(400)
Agent.refreshLinkedFile project_id,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, newFileId) ->
return LinkedFilesController.handleError(err, req, res, next) if err?
res.json(new_file_id: newFileId)
handleError: (error, req, res, next) ->
if error instanceof BadDataError
res.status(400).send("The submitted data is not valid")
else if error instanceof AccessDeniedError
res.status(403).send("You do not have access to this project")
else if error instanceof BadDataError
res.status(400).send("The submitted data is not valid")
else if error instanceof BadEntityTypeError
res.status(400).send("The file is the wrong type")
else if error instanceof SourceFileNotFoundError
res.status(404).send("Source file not found")
else if error instanceof ProjectNotFoundError
res.status(404).send("Project not found")
else if error instanceof V1ProjectNotFoundError
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
else if error instanceof OutputFileFetchFailedError
res.status(404).send("Could not get output file")
else if error instanceof UrlFetchFailedError
res.status(422).send(
"Your URL could not be reached (#{error.statusCode} status code). Please check it and try again."
)
else if error instanceof InvalidUrlError
res.status(422).send(
"Your URL is not valid. Please check it and try again."
)
else if error instanceof NotOriginalImporterError
res.status(400).send(
"You are not the user who originally imported this file"
)
else if error instanceof FeatureNotAvailableError
res.status(400).send(
"This feature is not enabled on your account"
)
else if error instanceof RemoteServiceError
res.status(502).send(
"The remote service produced an error"
)
else if error instanceof FileCannotRefreshError
res.status(400).send(
"This file cannot be refreshed"
)
else
next(error)
}

View File

@@ -1,120 +0,0 @@
UrlFetchFailedError = (message) ->
error = new Error(message)
error.name = 'UrlFetchFailedError'
error.__proto__ = UrlFetchFailedError.prototype
return error
UrlFetchFailedError.prototype.__proto__ = Error.prototype
InvalidUrlError = (message) ->
error = new Error(message)
error.name = 'InvalidUrlError'
error.__proto__ = InvalidUrlError.prototype
return error
InvalidUrlError.prototype.__proto__ = Error.prototype
OutputFileFetchFailedError = (message) ->
error = new Error(message)
error.name = 'OutputFileFetchFailedError'
error.__proto__ = OutputFileFetchFailedError.prototype
return error
OutputFileFetchFailedError.prototype.__proto__ = Error.prototype
AccessDeniedError = (message) ->
error = new Error(message)
error.name = 'AccessDenied'
error.__proto__ = AccessDeniedError.prototype
return error
AccessDeniedError.prototype.__proto__ = Error.prototype
BadEntityTypeError = (message) ->
error = new Error(message)
error.name = 'BadEntityType'
error.__proto__ = BadEntityTypeError.prototype
return error
BadEntityTypeError.prototype.__proto__ = Error.prototype
BadDataError = (message) ->
error = new Error(message)
error.name = 'BadData'
error.__proto__ = BadDataError.prototype
return error
BadDataError.prototype.__proto__ = Error.prototype
ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'ProjectNotFound'
error.__proto__ = ProjectNotFoundError.prototype
return error
ProjectNotFoundError.prototype.__proto__ = Error.prototype
V1ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'V1ProjectNotFound'
error.__proto__ = V1ProjectNotFoundError.prototype
return error
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
SourceFileNotFoundError = (message) ->
error = new Error(message)
error.name = 'SourceFileNotFound'
error.__proto__ = SourceFileNotFoundError.prototype
return error
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
NotOriginalImporterError = (message) ->
error = new Error(message)
error.name = 'NotOriginalImporter'
error.__proto__ = NotOriginalImporterError.prototype
return error
NotOriginalImporterError.prototype.__proto__ = Error.prototype
FeatureNotAvailableError = (message) ->
error = new Error(message)
error.name = 'FeatureNotAvailable'
error.__proto__ = FeatureNotAvailableError.prototype
return error
FeatureNotAvailableError.prototype.__proto__ = Error.prototype
RemoteServiceError = (message) ->
error = new Error(message)
error.name = 'RemoteService'
error.__proto__ = RemoteServiceError.prototype
return error
RemoteServiceError.prototype.__proto__ = Error.prototype
FileCannotRefreshError = (message) ->
error = new Error(message)
error.name = 'RemoteService'
error.__proto__ = FileCannotRefreshError.prototype
return error
FileCannotRefreshError.prototype.__proto__ = Error.prototype
module.exports = {
UrlFetchFailedError,
InvalidUrlError,
OutputFileFetchFailedError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
NotOriginalImporterError,
FeatureNotAvailableError,
RemoteServiceError,
FileCannotRefreshError
}

View File

@@ -1,86 +0,0 @@
FileWriter = require '../../infrastructure/FileWriter'
EditorController = require '../Editor/EditorController'
ProjectLocator = require '../Project/ProjectLocator'
Project = require("../../models/Project").Project
ProjectGetter = require("../Project/ProjectGetter")
_ = require 'underscore'
{
ProjectNotFoundError,
V1ProjectNotFoundError,
BadDataError
} = require './LinkedFilesErrors'
module.exports = LinkedFilesHandler =
getFileById: (project_id, file_id, callback=(err, file)->) ->
ProjectLocator.findElement {
project_id,
element_id: file_id,
type: 'file'
}, (err, file, path, parentFolder) ->
return callback(err) if err?
callback(null, file, path, parentFolder)
getSourceProject: (data, callback=(err, project)->) ->
projection = {_id: 1, name: 1}
if data.v1_source_doc_id?
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new V1ProjectNotFoundError())
callback(null, project)
else if data.source_project_id?
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new ProjectNotFoundError())
callback(null, project)
else
callback(new BadDataError('neither v1 nor v2 id present'))
importFromStream: (
project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
callback=(err, file)->
) ->
callback = _.once(callback)
FileWriter.writeStreamToDisk project_id, readStream, (err, fsPath) ->
return callback(err) if err?
EditorController.upsertFile project_id,
parent_folder_id,
name,
fsPath,
linkedFileData,
"upload",
user_id,
(err, file) =>
return callback(err) if err?
callback(null, file)
importContent: (
project_id,
content,
linkedFileData,
name,
parent_folder_id,
user_id,
callback=(err, file)->
) ->
callback = _.once(callback)
FileWriter.writeContentToDisk project_id, content, (err, fsPath) ->
return callback(err) if err?
EditorController.upsertFile project_id,
parent_folder_id,
name,
fsPath,
linkedFileData,
"upload",
user_id,
(err, file) =>
return callback(err) if err?
callback(null, file)

View File

@@ -1,28 +0,0 @@
AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware')
AuthenticationController = require('../Authentication/AuthenticationController')
RateLimiterMiddleware = require('../Security/RateLimiterMiddleware')
LinkedFilesController = require "./LinkedFilesController"
module.exports =
apply: (webRouter) ->
webRouter.post '/project/:project_id/linked_file',
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
RateLimiterMiddleware.rateLimit({
endpointName: "create-linked-file"
params: ["project_id"]
maxRequests: 100
timeInterval: 60
}),
LinkedFilesController.createLinkedFile
webRouter.post '/project/:project_id/linked_file/:file_id/refresh',
AuthenticationController.requireLogin(),
AuthorizationMiddleware.ensureUserCanWriteProjectContent,
RateLimiterMiddleware.rateLimit({
endpointName: "refresh-linked-file"
params: ["project_id"]
maxRequests: 100
timeInterval: 60
}),
LinkedFilesController.refreshLinkedFile

View File

@@ -1,122 +0,0 @@
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectLocator = require('../Project/ProjectLocator')
ProjectGetter = require('../Project/ProjectGetter')
DocstoreManager = require('../Docstore/DocstoreManager')
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
FileStoreHandler = require('../FileStore/FileStoreHandler')
_ = require "underscore"
Settings = require 'settings-sharelatex'
LinkedFilesHandler = require './LinkedFilesHandler'
{
BadDataError,
AccessDeniedError,
BadEntityTypeError,
SourceFileNotFoundError,
ProjectNotFoundError,
V1ProjectNotFoundError
} = require './LinkedFilesErrors'
module.exports = ProjectFileAgent = {
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
if !@_canCreate(linkedFileData)
return callback(new AccessDeniedError())
@_go(project_id, linkedFileData, name, parent_folder_id, user_id, callback)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@_go project_id, linkedFileData, name, parent_folder_id, user_id, callback
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
return callback(err) if err?
return callback(new AccessDeniedError()) if !allowed
if !@_validate(linkedFileData)
return callback(new BadDataError())
callback(null, linkedFileData)
_go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
linkedFileData = @_sanitizeData(linkedFileData)
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
if !@_validate(linkedFileData)
return callback(new BadDataError())
@_getEntity linkedFileData, user_id, (err, source_project, entity, type) =>
return callback(err) if err?
if type == 'doc'
DocstoreManager.getDoc source_project._id, entity._id, (err, lines) ->
return callback(err) if err?
LinkedFilesHandler.importContent project_id,
lines.join('\n'),
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else if type == 'file'
FileStoreHandler.getFileStream source_project._id, entity._id, null, (err, fileStream) ->
return callback(err) if err?
LinkedFilesHandler.importFromStream project_id,
fileStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
callback(new BadEntityTypeError())
_getEntity:
(linkedFileData, current_user_id, callback = (err, entity, type) ->) ->
callback = _.once(callback)
{ source_entity_path } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
DocumentUpdaterHandler.flushProjectToMongo source_project_id, (err) ->
return callback(err) if err?
ProjectLocator.findElementByPath {
project_id: source_project_id,
path: source_entity_path
exactCaseMatch: true
}, (err, entity, type) ->
if err?
if /^not found.*/.test(err.toString())
err = new SourceFileNotFoundError()
return callback(err)
callback(null, project, entity, type)
_sanitizeData: (data) ->
return _.pick(
data,
'provider',
'source_project_id',
'v1_source_doc_id',
'source_entity_path'
)
_validate: (data) ->
return (
(data.source_project_id? || data.v1_source_doc_id?) &&
data.source_entity_path?
)
_canCreate: (data) ->
# Don't allow creation of linked-files with v1 doc ids
!data.v1_source_doc_id?
_getSourceProject: LinkedFilesHandler.getSourceProject
_checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
@_getSourceProject data, (err, project) ->
return callback(err) if err?
AuthorizationManager.canUserReadProject current_user_id, project._id, null, (err, canRead) ->
return callback(err) if err?
callback(null, canRead)
}

View File

@@ -1,164 +0,0 @@
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectGetter = require('../Project/ProjectGetter')
Settings = require 'settings-sharelatex'
CompileManager = require '../Compile/CompileManager'
ClsiManager = require '../Compile/ClsiManager'
ProjectFileAgent = require './ProjectFileAgent'
_ = require "underscore"
{
BadDataError,
AccessDeniedError,
BadEntityTypeError,
OutputFileFetchFailedError
} = require './LinkedFilesErrors'
LinkedFilesHandler = require './LinkedFilesHandler'
logger = require 'logger-sharelatex'
module.exports = ProjectOutputFileAgent = {
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
return callback(err) if err?
return callback(new AccessDeniedError()) if !allowed
if !@_validate(linkedFileData)
return callback(new BadDataError())
callback(null, linkedFileData)
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
if !@_canCreate(linkedFileData)
return callback(new AccessDeniedError())
linkedFileData = @_sanitizeData(linkedFileData)
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
@_getFileStream linkedFileData, user_id, (err, readStream) =>
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) =>
if 200 <= response.statusCode < 300
readStream.resume()
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
err = new OutputFileFetchFailedError(
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
)
err.statusCode = response.statusCode
callback(err)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
@_compileAndGetFileStream linkedFileData, user_id, (err, readStream, new_build_id) =>
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) =>
if 200 <= response.statusCode < 300
readStream.resume()
linkedFileData.build_id = new_build_id
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
err = new OutputFileFetchFailedError(
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
)
err.statusCode = response.statusCode
callback(err)
_sanitizeData: (data) ->
return {
provider: data.provider,
source_project_id: data.source_project_id,
source_output_file_path: data.source_output_file_path,
build_id: data.build_id
}
_canCreate: ProjectFileAgent._canCreate
_getSourceProject: LinkedFilesHandler.getSourceProject
_validate: (data) ->
if data.v1_source_doc_id?
(
data.v1_source_doc_id? &&
data.source_output_file_path?
)
else
(
data.source_project_id? &&
data.source_output_file_path? &&
data.build_id?
)
_checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) ->
callback = _.once(callback)
if !@_validate(data)
return callback(new BadDataError())
@_getSourceProject data, (err, project) ->
return callback(err) if err?
AuthorizationManager.canUserReadProject current_user_id,
project._id,
null,
(err, canRead) ->
return callback(err) if err?
callback(null, canRead)
_getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) ->
callback = _.once(callback)
{ source_output_file_path, build_id } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
ClsiManager.getOutputFileStream source_project_id,
user_id,
build_id,
source_output_file_path,
(err, readStream) ->
return callback(err) if err?
readStream.pause()
callback(null, readStream)
_compileAndGetFileStream: (linkedFileData, user_id, callback=(err, stream, build_id)->) ->
callback = _.once(callback)
{ source_output_file_path } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
CompileManager.compile source_project_id,
user_id,
{},
(err, status, outputFiles) ->
return callback(err) if err?
if status != 'success'
return callback(new OutputFileFetchFailedError())
outputFile = _.find(
outputFiles,
(o) => o.path == source_output_file_path
)
if !outputFile?
return callback(new OutputFileFetchFailedError())
build_id = outputFile.build
ClsiManager.getOutputFileStream source_project_id,
user_id,
build_id,
source_output_file_path,
(err, readStream) ->
return callback(err) if err?
readStream.pause()
callback(null, readStream, build_id)
}

View File

@@ -1,51 +0,0 @@
request = require 'request'
_ = require "underscore"
urlValidator = require 'valid-url'
{ InvalidUrlError, UrlFetchFailedError } = require './LinkedFilesErrors'
LinkedFilesHandler = require './LinkedFilesHandler'
UrlHelper = require '../Helpers/UrlHelper'
module.exports = UrlAgent = {
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
linkedFileData = @._sanitizeData(linkedFileData)
@_getUrlStream project_id, linkedFileData, user_id, (err, readStream) ->
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) ->
if 200 <= response.statusCode < 300
readStream.resume()
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
error = new UrlFetchFailedError("url fetch failed: #{linkedFileData.url}")
error.statusCode = response.statusCode
callback(error)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@createLinkedFile project_id, linkedFileData, name, parent_folder_id, user_id, callback
_sanitizeData: (data) ->
return {
provider: data.provider
url: UrlHelper.prependHttpIfNeeded(data.url)
}
_getUrlStream: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
callback = _.once(callback)
url = data.url
if !urlValidator.isWebUri(url)
return callback(new InvalidUrlError("invalid url: #{url}"))
url = UrlHelper.wrapUrlWithProxy(url)
readStream = request.get(url)
readStream.pause()
callback(null, readStream)
}

View File

@@ -1,28 +0,0 @@
EditorRealTimeController = require "../Editor/EditorRealTimeController"
MetaHandler = require './MetaHandler'
logger = require 'logger-sharelatex'
module.exports = MetaController =
getMetadata: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "getting all labels for project"
MetaHandler.getAllMetaForProject project_id, (err, projectMeta) ->
if err?
logger.err {project_id, err}, "[MetaController] error getting all labels from project"
return next err
res.json {projectId: project_id, projectMeta: projectMeta}
broadcastMetadataForDoc: (req, res, next) ->
project_id = req.params.project_id
doc_id = req.params.doc_id
logger.log {project_id, doc_id}, "getting labels for doc"
MetaHandler.getMetaForDoc project_id, doc_id, (err, docMeta) ->
if err?
logger.err {project_id, doc_id, err}, "[MetaController] error getting labels from doc"
return next err
EditorRealTimeController.emitToRoom project_id, 'broadcastDocMeta', {
docId: doc_id, meta: docMeta
}
res.sendStatus 200

View File

@@ -1,66 +0,0 @@
ProjectEntityHandler = require "../Project/ProjectEntityHandler"
DocumentUpdaterHandler = require '../DocumentUpdater/DocumentUpdaterHandler'
packageMapping = require "./packageMapping"
module.exports = MetaHandler =
labelRegex: () ->
/\\label{(.{0,80}?)}/g
usepackageRegex: () ->
/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
ReqPackageRegex: () ->
/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g
getAllMetaForProject: (projectId, callback=(err, projectMeta)->) ->
DocumentUpdaterHandler.flushProjectToMongo projectId, (err) ->
if err?
return callback err
ProjectEntityHandler.getAllDocs projectId, (err, docs) ->
if err?
return callback err
projectMeta = MetaHandler.extractMetaFromProjectDocs docs
callback null, projectMeta
getMetaForDoc: (projectId, docId, callback=(err, docMeta)->) ->
DocumentUpdaterHandler.flushDocToMongo projectId, docId, (err) ->
if err?
return callback err
ProjectEntityHandler.getDoc projectId, docId, (err, lines) ->
if err?
return callback err
docMeta = MetaHandler.extractMetaFromDoc lines
callback null, docMeta
extractMetaFromDoc: (lines) ->
docMeta = {labels: [], packages: {}}
packages = []
label_re = MetaHandler.labelRegex()
package_re = MetaHandler.usepackageRegex()
req_package_re = MetaHandler.ReqPackageRegex()
for line in lines
while labelMatch = label_re.exec line
if label = labelMatch[1]
docMeta.labels.push label
while packageMatch = package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
while packageMatch = req_package_re.exec line
if messy = packageMatch[1]
for pkg in messy.split ','
if clean = pkg.trim()
packages.push clean
for pkg in packages
if packageMapping[pkg]?
docMeta.packages[pkg] = packageMapping[pkg]
return docMeta
extractMetaFromProjectDocs: (projectDocs) ->
projectMeta = {}
for _path, doc of projectDocs
projectMeta[doc._id] = MetaHandler.extractMetaFromDoc doc.lines
return projectMeta

File diff suppressed because one or more lines are too long

View File

@@ -1,89 +0,0 @@
async = require('async')
logger = require 'logger-sharelatex'
Settings = require 'settings-sharelatex'
crypto = require('crypto')
Mailchimp = require('mailchimp-api-v3')
if !Settings.mailchimp?.api_key?
logger.info "Using newsletter provider: none"
mailchimp =
request: (opts, cb)-> cb()
else
logger.info "Using newsletter provider: mailchimp"
mailchimp = new Mailchimp(Settings.mailchimp?.api_key)
module.exports =
subscribe: (user, callback = () ->)->
options = buildOptions(user, true)
logger.log options:options, user:user, email:user.email, "subscribing user to the mailing list"
mailchimp.request options, (err)->
if err?
logger.err err:err, user:user, "error subscribing person to newsletter"
else
logger.log user:user, "finished subscribing user to the newsletter"
callback(err)
unsubscribe: (user, callback = () ->)->
logger.log user:user, email:user.email, "trying to unsubscribe user to the mailing list"
options = buildOptions(user, false)
mailchimp.request options, (err)->
if err?
logger.err err:err, user:user, "error unsubscribing person to newsletter"
else
logger.log user:user, "finished unsubscribing user to the newsletter"
callback(err)
changeEmail: (oldEmail, newEmail, callback = ()->)->
options = buildOptions({email:oldEmail})
delete options.body.status
options.body.email_address = newEmail
logger.log {oldEmail, newEmail, options}, "changing email in newsletter"
mailchimp.request options, (err)->
if err? and err?.message?.indexOf("merge fields were invalid") != -1
logger.log {oldEmail, newEmail}, "unable to change email in newsletter, user has never subscribed"
return callback()
else if err? and err?.message?.indexOf("could not be validated") != -1
logger.log {oldEmail, newEmail},
"unable to change email in newsletter, user has previously unsubscribed or new email already exist on list"
return callback()
else if err? and err.message.indexOf("is already a list member") != -1
logger.log {oldEmail, newEmail},
"unable to change email in newsletter, new email is already on mailing list"
return callback()
else if err? and err?.message?.indexOf("looks fake or invalid") != -1
logger.log {oldEmail, newEmail},
"unable to change email in newsletter, email looks fake to mailchimp"
return callback()
else if err?
logger.err {err, oldEmail, newEmail}, "error changing email in newsletter"
return callback(err)
else
logger.log "finished changing email in the newsletter"
return callback()
hashEmail = (email)->
crypto.createHash('md5').update(email.toLowerCase()).digest("hex")
buildOptions = (user, is_subscribed)->
subscriber_hash = hashEmail(user.email)
status = if is_subscribed then "subscribed" else "unsubscribed"
opts =
method: "PUT"
path: "/lists/#{Settings.mailchimp?.list_id}/members/#{subscriber_hash}"
body:
email_address:user.email
status_if_new: status
#only set status if we explictly want to set it
if is_subscribed?
opts.body.status = status
if user._id?
opts.body.merge_fields =
FNAME: user.first_name
LNAME: user.last_name
MONGO_ID:user._id
return opts

View File

@@ -1,63 +0,0 @@
logger = require("logger-sharelatex")
NotificationsHandler = require("./NotificationsHandler")
request = require "request"
settings = require "settings-sharelatex"
module.exports =
# Note: notification keys should be url-safe
featuresUpgradedByAffiliation: (affiliation, user) ->
key: "features-updated-by=#{affiliation.institutionId}"
create: (callback=()->) ->
messageOpts =
institutionName: affiliation.institutionName
NotificationsHandler.createNotification user._id, @key, "notification_features_upgraded_by_affiliation", messageOpts, null, false, callback
read: (callback=()->) ->
NotificationsHandler.markAsRead @key, callback
redundantPersonalSubscription: (affiliation, user) ->
key: "redundant-personal-subscription-#{affiliation.institutionId}"
create: (callback=()->) ->
messageOpts =
institutionName: affiliation.institutionName
NotificationsHandler.createNotification user._id, @key, "notification_personal_subscription_not_required_due_to_affiliation", messageOpts, null, false, callback
read: (callback=()->) ->
NotificationsHandler.markAsRead @key, callback
projectInvite: (invite, project, sendingUser, user) ->
key: "project-invite-#{invite._id}"
create: (callback=()->) ->
messageOpts =
userName: sendingUser.first_name
projectName: project.name
projectId: project._id.toString()
token: invite.token
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
ipMatcherAffiliation: (userId) ->
create: (ip, callback=()->) ->
return null unless settings?.apis?.v1?.url # service is not configured
request {
method: 'GET'
url: "#{settings.apis.v1.url}/api/v2/users/#{userId}/ip_matcher"
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
body: { ip: ip }
json: true
timeout: 20 * 1000
}, (error, response, body) ->
return error if error?
return null unless response.statusCode == 200
key = "ip-matched-affiliation-#{body.id}"
messageOpts =
university_name: body.name
content: body.enrolment_ad_html
logger.log user_id:userId, key:key, "creating notification key for user"
NotificationsHandler.createNotification userId, key, "notification_ip_matched_affiliation", messageOpts, null, false, callback
read: (university_id, callback = ->)->
key = "ip-matched-affiliation-#{university_id}"
NotificationsHandler.markAsReadWithKey userId, key, callback

View File

@@ -1,21 +0,0 @@
NotificationsHandler = require("./NotificationsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
_ = require("underscore")
module.exports =
getAllUnreadNotifications: (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)->
unreadNotifications = _.map unreadNotifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
res.send(unreadNotifications)
markNotificationAsRead: (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
notification_id = req.params.notification_id
NotificationsHandler.markAsRead user_id, notification_id, ->
res.send()
logger.log user_id:user_id, notification_id:notification_id, "mark notification as read"

View File

@@ -1,80 +0,0 @@
settings = require("settings-sharelatex")
request = require("request")
logger = require("logger-sharelatex")
oneSecond = 1000
makeRequest = (opts, callback)->
if !settings.apis.notifications?.url?
return callback(null, statusCode:200)
else
request(opts, callback)
module.exports =
getUserNotifications: (user_id, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
json: true
timeout: oneSecond
method: "GET"
makeRequest opts, (err, res, unreadNotifications)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
logger.err err:err, "something went wrong getting notifications"
callback(null, [])
else
if !unreadNotifications?
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
if !callback
callback = forceCreate
forceCreate = true
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
forceCreate:forceCreate
}
if expiryDateTime?
payload.expires = expiryDateTime
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
json: payload
logger.log opts:opts, "creating notification for user"
makeRequest opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
method: "DELETE"
timeout: oneSecond
json: {
key:key
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
makeRequest opts, callback
markAsRead: (user_id, notification_id, callback)->
opts =
method: "DELETE"
uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
makeRequest opts, callback
# removes notification by key, without regard for user_id,
# should not be exposed to user via ui/router
markAsReadByKeyOnly: (key, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/key/#{key}"
method: "DELETE"
timeout: oneSecond
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
makeRequest opts, callback

View File

@@ -1,83 +0,0 @@
PasswordResetHandler = require("./PasswordResetHandler")
RateLimiter = require("../../infrastructure/RateLimiter")
AuthenticationController = require("../Authentication/AuthenticationController")
AuthenticationManager = require("../Authentication/AuthenticationManager")
UserGetter = require("../User/UserGetter")
UserUpdater = require("../User/UserUpdater")
UserSessionsManager = require("../User/UserSessionsManager")
logger = require "logger-sharelatex"
Settings = require 'settings-sharelatex'
module.exports =
renderRequestResetForm: (req, res)->
logger.log "rendering request reset form"
res.render "user/passwordReset",
title:"reset_password"
requestReset: (req, res)->
email = req.body.email.trim().toLowerCase()
opts =
endpointName: "password_reset_rate_limit"
timeInterval: 60
subjectName: req.ip
throttle: 6
RateLimiter.addCount opts, (err, canContinue)->
if !canContinue
return res.send 429, { message: req.i18n.translate("rate_limit_hit_wait")}
PasswordResetHandler.generateAndEmailResetToken email, (err, status)->
if err?
res.send 500, {message:err?.message}
else if status == 'primary'
res.send 200, {message: {text: req.i18n.translate("password_reset_email_sent")}}
else if status == 'secondary'
res.send 404, {message: req.i18n.translate("secondary_email_password_reset")}
else if status == 'sharelatex'
res.send 404, {message: "<a href=\"#{Settings.accountMerge.sharelatexHost}/user/password/reset\">#{req.i18n.translate("reset_from_sl")}</a>"}
else
res.send 404, {message: req.i18n.translate("cant_find_email")}
renderSetPasswordForm: (req, res)->
if req.query.passwordResetToken?
req.session.resetToken = req.query.passwordResetToken
return res.redirect('/user/password/set')
if !req.session.resetToken?
return res.redirect('/user/password/reset')
res.render "user/setPassword",
title:"set_password"
passwordResetToken: req.session.resetToken
setNewUserPassword: (req, res, next)->
{passwordResetToken, password} = req.body
if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 or AuthenticationManager.validatePassword(password?.trim())?
return res.sendStatus 400
delete req.session.resetToken
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
if err and err.name and err.name == "NotFoundError"
res.status(404).send("NotFoundError")
else if err and err.name and err.name == "NotInV2Error"
res.status(403).send("NotInV2Error")
else if err and err.name and err.name == "SLInV2Error"
res.status(403).send("SLInV2Error")
else if err and err.statusCode and err.statusCode == 500
res.status(500)
else if err and !err.statusCode
res.status(500)
else if found
return res.sendStatus 200 if !user_id? # will not exist for v1-only users
UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) ->
return next(err) if err?
UserUpdater.removeReconfirmFlag user_id, (err) ->
return next(err) if err?
if req.body.login_after
UserGetter.getUser user_id, {email: 1}, (err, user) ->
return next(err) if err?
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
if err?
logger.err {err, email: user.email}, "Error setting up session after setting password"
return next(err)
res.json {redir: AuthenticationController._getRedirectFromSession(req) || "/project"}
else
res.sendStatus 200
else
res.sendStatus 404

View File

@@ -1,76 +0,0 @@
settings = require("settings-sharelatex")
async = require("async")
UserGetter = require("../User/UserGetter")
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
EmailHandler = require("../Email/EmailHandler")
AuthenticationManager = require("../Authentication/AuthenticationManager")
logger = require("logger-sharelatex")
V1Api = require("../V1/V1Api")
module.exports = PasswordResetHandler =
generateAndEmailResetToken:(email, callback = (error, status) ->)->
PasswordResetHandler._getPasswordResetData email, (error, exists, data) ->
if error?
return callback(error, null)
else if exists
OneTimeTokenHandler.getNewToken 'password', data, (err, token)->
if err then return callback(err)
emailOptions =
to : email
setNewPasswordUrl : "#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}&email=#{encodeURIComponent(email)}"
EmailHandler.sendEmail "passwordResetRequested", emailOptions, (error) ->
return callback(error) if error?
callback null, 'primary'
else
UserGetter.getUserByAnyEmail email, (err, user) ->
if !user
return callback(error, null)
else if !user.overleaf?.id?
return callback(error, 'sharelatex')
else
return callback(error, 'secondary')
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, data)->
if err then return callback(err)
if !data?
return callback null, false, null
if typeof data == "string"
# Backwards compatible with old format.
# Tokens expire after 1h, so this can be removed soon after deploy.
# Possibly we should keep this until we do an onsite release too.
data = { user_id: data }
if data.user_id?
AuthenticationManager.setUserPassword data.user_id, password, (err, reset) ->
if err then return callback(err)
callback null, reset, data.user_id
else if data.v1_user_id?
AuthenticationManager.setUserPasswordInV1 data.v1_user_id, password, (error, reset) ->
return callback(error) if error?
UserGetter.getUser { 'overleaf.id': data.v1_user_id }, {_id:1}, (error, user) ->
return callback(error) if error?
callback null, reset, user?._id
_getPasswordResetData: (email, callback = (error, exists, data) ->) ->
if settings.overleaf?
# Overleaf v2
V1Api.request {
url: "/api/v1/sharelatex/user_emails"
qs:
email: email
expectedStatusCodes: [404]
}, (error, response, body) ->
return callback(error) if error?
if response.statusCode == 404
return callback null, false
else
return callback null, true, { v1_user_id: body.user_id }
else
# ShareLaTeX
UserGetter.getUserByMainEmail email, (err, user)->
if err then return callback(err)
if !user? or user.holdingAccount or user.overleaf?
logger.err email:email, "user could not be found for password reset"
return callback(null, false)
return callback null, true, { user_id: user._id }

View File

@@ -1,15 +0,0 @@
PasswordResetController = require("./PasswordResetController")
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
apply: (webRouter, apiRouter) ->
webRouter.get '/user/password/reset', PasswordResetController.renderRequestResetForm
webRouter.post '/user/password/reset', PasswordResetController.requestReset
AuthenticationController.addEndpointToLoginWhitelist '/user/password/reset'
webRouter.get '/user/password/set', PasswordResetController.renderSetPasswordForm
webRouter.post '/user/password/set', PasswordResetController.setNewUserPassword
AuthenticationController.addEndpointToLoginWhitelist '/user/password/set'
webRouter.post '/user/reconfirm', PasswordResetController.requestReset

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