mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-29 12:01:32 +02:00
Merge pull request #1717 from overleaf/as-decaffeinate-backend
Decaffeinate backend GitOrigin-RevId: 4ca9f94fc809cab6f47cec8254cacaf1bb3806fa
This commit is contained in:
committed by
sharelatex
parent
d4eb71b525
commit
0ca81de78c
@@ -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
|
||||
|
||||
17
services/web/.gitignore
vendored
17
services/web/.gitignore
vendored
@@ -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/*
|
||||
|
||||
@@ -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
|
||||
|
||||
2
services/web/.vscode/settings.json
vendored
2
services/web/.vscode/settings.json
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
86
services/web/Gruntfile.js
Normal 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']
|
||||
)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
77
services/web/app.js
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports =
|
||||
NONE: false
|
||||
READ_ONLY: "readOnly"
|
||||
READ_AND_WRITE: "readAndWrite"
|
||||
OWNER: "owner"
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports =
|
||||
READ_ONLY: "readOnly" # LEGACY
|
||||
READ_AND_WRITE: "readAndWrite" # LEGACY
|
||||
PRIVATE: "private"
|
||||
TOKEN_BASED: "tokenBased"
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports =
|
||||
INVITE: 'invite'
|
||||
TOKEN: 'token'
|
||||
OWNER: 'owner'
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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}"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
@@ -1,9 +0,0 @@
|
||||
AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
ContactController = require "./ContactController"
|
||||
|
||||
module.exports =
|
||||
apply: (webRouter, apiRouter) ->
|
||||
webRouter.get '/user/contacts',
|
||||
AuthenticationController.requireLogin(),
|
||||
ContactController.getContacts
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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...
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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))
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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
Reference in New Issue
Block a user