Merge branch 'master' into ha-docker

# Conflicts:
#	app/coffee/Features/Email/EmailBuilder.coffee
This commit is contained in:
Shane Kilkelly 2017-01-24 10:24:35 +00:00
commit 9a3c1c7d22
950 changed files with 395957 additions and 41085 deletions

View file

@ -47,23 +47,21 @@ TpdsWorker.js
BackgroundJobsWorker.js
UserAndProjectPopulator.coffee
public/js/history/versiondetail.js
!public/js/libs/
public/js/*
!public/js/ace/*
!public/js/libs/
public/js/*.js
public/js/libs/sharejs.js
public/js/editor.js
public/js/home.js
public/js/forms.js
public/js/gui.js
public/js/admin.js
public/js/history/*
public/js/analytics/
public/js/directives/
public/js/filters/
public/js/ide/
public/js/main/
public/js/modules/
public/js/services/
public/js/utils/
public/stylesheets/style.css
public/brand/plans.css
public/minjs/
public/js/main.js
Gemfile.lock
*.swp

View file

@ -1,4 +1,5 @@
fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-coffee'
@ -157,12 +158,13 @@ module.exports = (grunt) ->
inlineText: false
preserveLicenseComments: false
paths:
"moment": "libs/moment-2.9.0"
"moment": "libs/#{PackageVersions.lib('moment')}"
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
"ace": "#{PackageVersions.lib('ace')}"
shim:
"libs/pdf":
deps: ["libs/pdfjs-1.3.91/compatibility"]
"pdfjs-dist/build/pdf":
deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]
skipDirOptimize: true
modules: [
@ -171,11 +173,13 @@ module.exports = (grunt) ->
exclude: ["libs"]
}, {
name: "ide",
exclude: ["libs", "libs/pdf"]
exclude: ["libs", "pdfjs-dist/build/pdf"]
}, {
name: "libs"
},{
name: "ace/mode-latex"
},{
name: "ace/worker-latex"
}
]
@ -380,8 +384,8 @@ module.exports = (grunt) ->
grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
grunt.registerTask 'runq', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
grunt.registerTask 'run:watch', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
grunt.registerTask 'default', 'run'

View file

@ -4,4 +4,4 @@ module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
return next(error) if error?
res.send 204
res.send 204

View file

@ -1,43 +1,48 @@
Settings = require "settings-sharelatex"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
_ = require "underscore"
request = require "request"
if !Settings.analytics?.postgres?
module.exports =
recordEvent: (user_id, event, segmentation, callback = () ->) ->
logger.log {user_id, event, segmentation}, "no event tracking configured, logging event"
callback()
else
Sequelize = require "sequelize"
options = _.extend {logging:false}, Settings.analytics.postgres
sequelize = new Sequelize(
Settings.analytics.postgres.database,
Settings.analytics.postgres.username,
Settings.analytics.postgres.password,
options
)
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
segmentation: Sequelize.JSONB
})
makeRequest = (opts, callback)->
if settings.apis?.analytics?.url?
urlPath = opts.url
opts.url = "#{settings.apis.analytics.url}#{urlPath}"
request opts, callback
else
callback()
module.exports =
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id? and typeof(user_id) != "string"
user_id = user_id.toString()
if user_id == Settings.smokeTest?.userId
# Don't record smoke tests analytics
return callback()
Event
.create({ user_id, event, segmentation })
.then(
(result) -> callback(),
(error) ->
logger.err {err: error, user_id, event, segmentation}, "error recording analytics event"
callback(error)
)
sync: () -> sequelize.sync()
module.exports =
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id+"" == settings.smokeTest?.userId+""
return callback()
opts =
body:
event:event
segmentation:segmentation
json:true
method:"POST"
timeout:1000
url: "/user/#{user_id}/event"
makeRequest opts, callback
getLastOccurance: (user_id, event, callback = (error) ->) ->
opts =
body:
event:event
json:true
method:"POST"
timeout:1000
url: "/user/#{user_id}/event/last_occurnace"
makeRequest opts, (err, response, body)->
if err?
console.log response, opts
logger.err {user_id, err}, "error getting last occurance of event"
return callback err
else
return callback null, body

View file

@ -0,0 +1,24 @@
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_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "getting unread announcements"
AnnouncementsHandler.getUnreadAnnouncements user_id, (err, announcements)->
if err?
logger.err {err, user_id}, "unable to get unread announcements"
next(err)
else
res.json announcements

View file

@ -0,0 +1,40 @@
AnalyticsManager = require("../Analytics/AnalyticsManager")
BlogHandler = require("../Blog/BlogHandler")
async = require("async")
_ = require("lodash")
logger = require("logger-sharelatex")
settings = require("settings-sharelatex")
module.exports =
getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
async.parallel {
lastEvent: (cb)->
AnalyticsManager.getLastOccurance 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)
announcements = _.sortBy(results.announcements, "date").reverse()
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
announcementIndex = _.findIndex announcements, (announcement)->
announcement.id == lastSeenBlogId
announcements = _.map announcements, (announcement, index)->
if announcementIndex == -1
read = false
else if index >= announcementIndex
read = true
else
read = false
announcement.read = read
return announcement
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements"
callback null, announcements

View file

@ -11,63 +11,126 @@ basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
passport = require 'passport'
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
AuthenticationController.doLogin req.body, req, res, next
doLogin: (options, req, res, next) ->
email = options.email?.toLowerCase()
password = options.password
redir = Url.parse(options.redir or "/project").path
serializeUser: (user, callback) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
email: user.email
referal_id: user.referal_id
session_created: (new Date()).toISOString()
ip_address: user._login_req_ip
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
# 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
redir = AuthenticationController._getRedirectFromSession(req) || "/project"
AuthenticationController.afterLoginSessionSetup req, user, (err) ->
if err?
return next(err)
AuthenticationController._clearRedirectFromSession(req)
res.json {redir: redir}
else
res.json message: info
)(req, res, next)
doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase()
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
res.statusCode = 429
return res.send
message:
text: req.i18n.translate("to_many_login_requests_2_mins"),
type: 'error'
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return next(error) if error?
return done(error) if error?
if user?
UserHandler.setupLoginData user, ->
LoginRateLimiter.recordSuccessfulLogin email
AuthenticationController._recordSuccessfulLogin user._id
AuthenticationController.establishUserSession req, user, (error) ->
return next(error) if error?
req.session.justLoggedIn = true
logger.log email: email, user_id: user._id.toString(), "successful log in"
Analytics.recordEvent user._id, "user-logged-in"
res.json redir: redir
# async actions
UserHandler.setupLoginData(user, ()->)
LoginRateLimiter.recordSuccessfulLogin(email)
AuthenticationController._recordSuccessfulLogin(user._id)
Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
logger.log email: 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
return done(null, user)
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
res.json message:
text: req.i18n.translate("email_or_password_wrong_try_again"),
type: 'error'
return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
getLoggedInUserId: (req, callback = (error, user_id) ->) ->
if req?.session?.user?._id?
callback null, req.session.user._id.toString()
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
callback null, null
return null
getLoggedInUser: (req, callback = (error, user) ->) ->
if req.session?.user?._id?
query = req.session.user._id
getSessionUser: (req) ->
if req?.session?.user?
return req.session.user
else if req?.session?.passport?.user
return req.session.passport.user
else
return callback null, null
UserGetter.getUser query, callback
return null
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
if !req.session.user?
if !AuthenticationController.isUserLoggedIn(req)
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
req.user = req.session.user
return next()
req.user = AuthenticationController.getSessionUser(req)
next()
return doRequest
@ -81,10 +144,11 @@ module.exports = AuthenticationController =
if req.headers['authorization']?
return AuthenticationController.httpAuth(req, res, next)
else if req.session.user?
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)->
@ -94,22 +158,23 @@ module.exports = AuthenticationController =
return isValid
_redirectToLoginOrRegisterPage: (req, res)->
if req.query.zipUrl? or req.query.project_name?
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"
req.query.redir = req.path
AuthenticationController._setRedirectInSession(req)
url = "/login?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
_redirectToRegisterPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to register page"
req.query.redir = req.path
AuthenticationController._setRedirectInSession(req)
url = "/register?#{querystring.stringify(req.query)}"
res.redirect url
Metrics.inc "security.login-redirect"
@ -127,25 +192,15 @@ module.exports = AuthenticationController =
Metrics.inc "user.login.failed"
callback()
establishUserSession: (req, user, callback = (error) ->) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
email: user.email
referal_id: user.referal_id
session_created: (new Date()).toISOString()
ip_address: req.ip
# Regenerate the session to get a new sessionID (cookie value) to
# protect against session fixation attacks
oldSession = req.session
req.session.destroy()
req.sessionStore.generate(req)
for key, value of oldSession
req.session[key] = value
_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? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
req.session.postLoginRedirect = value
req.session.user = lightUser
_getRedirectFromSession: (req) ->
return req?.session?.postLoginRedirect || null
UserSessionsManager.trackSession(user, req.sessionID, () ->)
callback()
_clearRedirectFromSession: (req) ->
if req.session?
delete req.session.postLoginRedirect

View file

@ -29,6 +29,9 @@ module.exports = AuthenticationManager =
callback null, null
setUserPassword: (user_id, password, callback = (error) ->) ->
if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length
return callback("password is too long")
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
return callback(error) if error?
bcrypt.hash password, salt, (error, hash) ->

View file

@ -3,6 +3,7 @@ async = require "async"
logger = require "logger-sharelatex"
ObjectId = require("mongojs").ObjectId
Errors = require "../Errors/Errors"
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = AuthorizationMiddlewear =
ensureUserCanReadMultipleProjects: (req, res, next) ->
@ -20,7 +21,7 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear.redirectToRestricted req, res, next
else
next()
ensureUserCanReadProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@ -32,7 +33,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user read access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanWriteProjectSettings: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@ -44,7 +45,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanWriteProjectContent: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@ -56,7 +57,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanAdminProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@ -68,7 +69,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user admin access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserIsSiteAdmin: (req, res, next) ->
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return next(error) if error?
@ -90,22 +91,22 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return callback(error) if error?
callback(null, user_id, project_id)
_getUserId: (req, callback = (error, user_id) ->) ->
if req.session?.user?._id?
user_id = req.session.user._id
else
user_id = null
callback null, user_id
user_id = AuthenticationController.getLoggedInUserId(req)
return callback(null, user_id)
redirectToRestricted: (req, res, next) ->
res.redirect "/restricted"
res.redirect "/restricted?from=#{encodeURIComponent(req.url)}"
restricted : (req, res, next)->
if req.session.user?
if AuthenticationController.isUserLoggedIn(req)
res.render 'user/restricted',
title:'restricted'
else
logger.log "user not logged in and trying to access #{req.url}, being redirected to login"
res.redirect '/register'
from = req.query.from
logger.log {from: from}, "redirecting to login"
redirect_to = "/login"
if from?
AuthenticationController._setRedirectInSession(req, from)
res.redirect redirect_to

View file

@ -2,14 +2,15 @@ BetaProgramHandler = require './BetaProgramHandler'
UserLocator = require "../User/UserLocator"
Settings = require "settings-sharelatex"
logger = require 'logger-sharelatex'
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = BetaProgramController =
optIn: (req, res, next) ->
user_id = req?.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting in to beta program"
if !user_id
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optIn user_id, (err) ->
if err
@ -17,9 +18,9 @@ module.exports = BetaProgramController =
return res.redirect "/beta/participate"
optOut: (req, res, next) ->
user_id = req?.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting out of beta program"
if !user_id
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optOut user_id, (err) ->
if err
@ -27,7 +28,7 @@ module.exports = BetaProgramController =
return res.redirect "/beta/participate"
optInPage: (req, res, next)->
user_id = req.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "showing beta participation page for user"
UserLocator.findById user_id, (err, user)->
if err

View file

@ -28,7 +28,7 @@ module.exports = BlogController =
try
data = JSON.parse(data)
if settings.cdn?.web?.host?
data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
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

View file

@ -0,0 +1,24 @@
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:500
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.url = "/blog#{announcement.url}"
announcement.date = new Date(announcement.date)
return announcement
callback(err, announcements)

View file

@ -0,0 +1,61 @@
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

View file

@ -1,29 +1,34 @@
ChatHandler = require("./ChatHandler")
ChatApiHandler = require("./ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
CommentsController = require('../Comments/CommentsController')
module.exports =
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, (err)->
res.send(204)
sendMessage: (req, res)->
project_id = req.params.Project_id
user_id = req.session.user._id
messageContent = req.body.content
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
if err?
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
return res.sendStatus(500)
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)->
res.send()
getMessages: (req, res)->
project_id = req.params.Project_id
getMessages: (req, res, next)->
project_id = req.params.project_id
query = req.query
logger.log project_id:project_id, query:query, "getting messages"
ChatHandler.getMessages project_id, query, (err, messages)->
if err?
logger.err err:err, query:query, "problem getting messages from chat api"
return res.sendStatus 500
logger.log length:messages?.length, "sending messages to client"
res.set 'Content-Type', 'application/json'
res.send messages
ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
return next(err) if err?
CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
return next(err) if err?
logger.log length: messages?.length, "sending messages to client"
res.json messages

View file

@ -1,32 +0,0 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
sendMessage: (project_id, user_id, messageContent, callback)->
opts =
method:"post"
json:
content:messageContent
user_id:user_id
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
request opts, (err, response, body)->
if err?
logger.err err:err, "problem sending new message to chat"
callback(err, body)
getMessages: (project_id, query, callback)->
qs = {}
qs.limit = query.limit if query?.limit?
qs.before = query.before if query?.before?
opts =
uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages"
method:"get"
qs: qs
request opts, (err, response, body)->
callback(err, body)

View file

@ -11,28 +11,7 @@ module.exports = CollaboratorsEmailHandler =
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
notifyUserOfProjectShare: (project_id, email, 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
url: "#{Settings.siteUrl}/project/#{project._id}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
"new_email=#{encodeURIComponent(email)}"
"r=#{project.owner_ref.referal_id}" # Referal
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
@ -45,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
sendingUser_id: sendingUser._id
EmailHandler.sendEmail "projectInvite", emailOptions, callback

View file

@ -4,10 +4,13 @@ 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")
module.exports = CollaboratorsInviteController =
@ -20,11 +23,36 @@ module.exports = CollaboratorsInviteController =
return next(err)
res.json({invites: invites})
_checkShouldInviteEmail: (sendingUser, email, callback=(err, shouldAllowInvite)->) ->
if Settings.restrictInvitesToExistingAccounts == true
logger.log {email}, "checking if user exists with this email"
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
return callback(err) if err?
userExists = user? and user?._id?
callback(null, userExists)
else
UserGetter.getUser sendingUser._id, {features:1, _id:1}, (err, user)->
if err?
return callback(err)
collabLimit = user?.features?.collaborators || 1
if collabLimit == -1
collabLimit = 20
collabLimit = collabLimit * 10
opts =
endpointName: "invite_to_project"
timeInterval: 60 * 30
subjectName: sendingUser._id
throttle: collabLimit
rateLimiter.addCount opts, callback
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
sendingUser = req.session.user
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?
@ -36,13 +64,20 @@ module.exports = CollaboratorsInviteController =
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
CollaboratorsInviteController._checkShouldInviteEmail sendingUser, email, (err, shouldAllowInvite)->
if err?
logger.err {projectId, email, sendingUserId}, "error creating project invite"
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address"
return next(err)
logger.log {projectId, email, sendingUserId}, "invite created"
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
return res.json {invite: invite}
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
@ -58,8 +93,8 @@ module.exports = CollaboratorsInviteController =
resendInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
sendingUser = req.session.user
logger.log {projectId, inviteId}, "resending invite"
sendingUser = AuthenticationController.getSessionUser(req)
CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite"
@ -69,11 +104,11 @@ module.exports = CollaboratorsInviteController =
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
currentUser = req.session.user
_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.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
logger.err {err, projectId}, "error checking if user is member of project"
@ -111,14 +146,16 @@ module.exports = CollaboratorsInviteController =
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
{token} = req.body
currentUser = req.session.user
logger.log {projectId, inviteId, userId: currentUser._id}, "accepting invite"
CollaboratorsInviteHandler.acceptInvite projectId, inviteId, token, currentUser, (err) ->
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, inviteId}, "error accepting invite by token"
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", {inviteId:inviteId, projectId:projectId})
res.redirect "/project/#{projectId}"
AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id})
if req.xhr
res.sendStatus 204 # Done async via project page notification
else
res.redirect "/project/#{projectId}"

View file

@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, sendingUser, (err)->
return callback(err) if err?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err?
@ -77,10 +77,12 @@ module.exports = CollaboratorsInviteHandler =
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 {projectId, email}, "error sending messages for invite"
callback(err, invite)
callback(null, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "removing invite"
@ -102,7 +104,7 @@ module.exports = CollaboratorsInviteHandler =
return callback(null)
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
logger.err {projectid, inviteId}, "error resending invite messages"
logger.err {projectId, inviteId}, "error resending invite messages"
return callback(err)
callback(null)
@ -117,15 +119,15 @@ module.exports = CollaboratorsInviteHandler =
return callback(null, null)
callback(null, invite)
acceptInvite: (projectId, inviteId, tokenString, user, callback=(err)->) ->
logger.log {projectId, inviteId, userId: user._id}, "accepting 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, inviteId}, "error finding invite"
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, inviteId, tokenString}, "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) ->

View file

@ -24,7 +24,13 @@ module.exports =
RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project"
params: ["Project_id"]
maxRequests: 200
maxRequests: 100
timeInterval: 60 * 10
}),
RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project-ip"
ipOnly:true
maxRequests: 100
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
@ -66,7 +72,7 @@ module.exports =
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/accept',
'/project/:Project_id/invite/token/:token/accept',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite
)

View file

@ -0,0 +1,83 @@
ChatApiHandler = require("../Chat/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 = CommentsController =
sendComment: (req, res, next) ->
{project_id, thread_id} = req.params
content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
logger.log {project_id, thread_id, user_id, content}, "sending comment"
ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) ->
return next(err) if err?
UserInfoManager.getPersonalInfo comment.user_id, (err, user) ->
return next(err) if err?
comment.user = UserInfoController.formatPersonalInfo(user)
EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) ->
res.send 204
getThreads: (req, res, next) ->
{project_id} = req.params
logger.log {project_id}, "getting comment threads for project"
ChatApiHandler.getThreads project_id, (err, threads) ->
return next(err) if err?
CommentsController._injectUserInfoIntoThreads threads, (error, threads) ->
return next(err) if err?
res.json threads
resolveThread: (req, res, next) ->
{project_id, thread_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, thread_id, user_id}, "resolving comment thread"
ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) ->
return next(err) if err?
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)->
res.send 204
reopenThread: (req, res, next) ->
{project_id, thread_id} = req.params
logger.log {project_id, thread_id}, "reopening comment thread"
ChatApiHandler.reopenThread project_id, thread_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)->
res.send 204
_injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) ->
userCache = {}
getUserDetails = (user_id, callback = (error, user) ->) ->
return callback(null, userCache[user_id]) if userCache[user_id]?
UserInfoManager.getPersonalInfo user_id, (err, user) ->
return callback(error) if error?
user = UserInfoController.formatPersonalInfo user
userCache[user_id] = user
callback null, user
jobs = []
for thread_id, thread of threads
do (thread) ->
if thread.resolved
jobs.push (cb) ->
getUserDetails thread.resolved_by_user_id, (error, user) ->
cb(error) if error?
thread.resolved_by_user = user
cb()
for message in thread.messages
do (message) ->
jobs.push (cb) ->
getUserDetails message.user_id, (error, user) ->
cb(error) if error?
message.user = user
cb()
async.series jobs, (error) ->
return callback(error) if error?
return callback null, threads

View file

@ -48,6 +48,10 @@ module.exports = ClsiCookieManager =
multi.exec (err)->
callback(err, serverId)
clearServerId: (project_id, callback = (err)->)->
if !clsiCookiesEnabled
return callback()
rclient.del buildKey(project_id), callback
getCookieJar: (project_id, callback = (err, jar)->)->
if !clsiCookiesEnabled

View file

@ -16,53 +16,53 @@ module.exports = CompileController =
res.setTimeout(5 * 60 * 1000)
project_id = req.params.Project_id
isAutoCompile = !!req.query?.auto_compile
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
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
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?
options = {
isAutoCompile: isAutoCompile
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
compileGroup: limits?.compileGroup
clsiServerId:clsiServerId
validationProblems:validationProblems
}
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
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
}
stopCompile: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
AuthenticationController.getLoggedInUserId req, (error, user_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?
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()
res.status(200).send()
_compileAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
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
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
user_id = AuthenticationController.getLoggedInUserId req
return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
@ -151,9 +151,9 @@ module.exports = CompileController =
{page, h, v} = req.query
if not page?.match(/^\d+$/)
return next(new Error("invalid page parameter"))
if not h?.match(/^\d+\.\d+$/)
if not h?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid h parameter"))
if not v?.match(/^\d+\.\d+$/)
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) ->

View file

@ -6,33 +6,32 @@ Modules = require "../../infrastructure/Modules"
module.exports = ContactsController =
getContacts: (req, res, next) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
user_id = AuthenticationController.getLoggedInUserId req
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
return next(error) if error?
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
UserGetter.getUsers contact_ids, {
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
}, (error, contacts) ->
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
})
# 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()

View file

@ -29,8 +29,23 @@ module.exports = DocstoreManager =
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) ->) ->
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
if typeof(options) == "function"
callback = options
options = {}
@ -45,19 +60,21 @@ module.exports = DocstoreManager =
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)
callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
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, callback = (error, modified, rev) ->) ->
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

View file

@ -95,7 +95,7 @@ module.exports = DocumentUpdaterHandler =
logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error)
getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) ->
getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) ->
timer = new metrics.Timer("get-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}"
logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater"
@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler =
body = JSON.parse(body)
catch error
return callback(error)
callback null, body.lines, body.version, body.ops
callback null, body.lines, body.version, body.ranges, body.ops
else
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
@ -137,15 +137,21 @@ module.exports = DocumentUpdaterHandler =
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
getNumberOfDocsInMemory : (callback)->
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)->
try
body = JSON.parse body
catch err
logger.err err:err, "error parsing response from doc updater about the total number of docs"
callback(err, body?.total)
acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
timer = new metrics.Timer("accept-change")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
request.post url, (error, res, body)->
timer.done()
if error?
logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater"
return callback(error)
if res.statusCode >= 200 and res.statusCode < 300
logger.log {project_id, doc_id, change_id}, "accepted change in document updater"
return callback(null)
else
logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
PENDINGUPDATESKEY = "PendingUpdates"
DOCLINESKEY = "doclines"

View file

@ -7,7 +7,7 @@ module.exports =
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)"
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev) ->
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 element for getDocument"
return next(error)
@ -18,14 +18,16 @@ module.exports =
res.type "json"
res.send JSON.stringify {
lines: lines
version: version
ranges: ranges
}
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
lines = req.body.lines
{lines, version, ranges} = req.body
logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)"
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, (error) ->
ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)

View file

@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
EditorRealTimeController = require("./EditorRealTimeController")
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
async = require('async')
LockManager = require("../../infrastructure/LockManager")
_ = require('underscore')

View file

@ -0,0 +1,49 @@
_ = require("underscore")
settings = require "settings-sharelatex"
module.exports = _.template """
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
<h3 class="avoid-auto-linking" style="Margin: 0; Margin-bottom: px; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: px; padding: 0; text-align: left; word-wrap: normal;">
<%= title %>
</h3>
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= greeting %>
</p>
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= message %>
</p>
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<center data-parsed="" style="min-width: 532px; width: 100%;">
<table class="button float-center" style="Margin: 0 0 16px 0; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 0 16px 0; padding: 0; text-align: center; vertical-align: top; width: auto;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #a93529; border: 2px solid #a93529; border-collapse: collapse !important; color: #fefefe; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
<a href="<%= ctaURL %>" style="Margin: 0; border: 0 solid #a93529; border-radius: 3px; color: #fefefe; display: inline-block; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: bold; line-height: 1.3; margin: 0; padding: 8px 16px 8px 16px; text-align: left; text-decoration: none;">
<%= ctaText %>
</a>
</td></tr></table></td></tr></table>
</center>
<% if (secondaryMessage) { %>
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<p class="avoid-auto-linking" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
<%= secondaryMessage %>
</p>
<% } %>
</th>
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
</tr></tbody></table>
<% if (gmailGoToAction) { %>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "EmailMessage",
"potentialAction": {
"@type": "ViewAction",
"target": "<%= gmailGoToAction.target %>",
"url": "<%= gmailGoToAction.target %>",
"name": "<%= gmailGoToAction.name %>"
},
"description": "<%= gmailGoToAction.description %>"
}
</script>
<% } %>
"""

View file

@ -1,16 +1,30 @@
_ = require('underscore')
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
settings = require("settings-sharelatex")
templates = {}
templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
plainTextTemplate: _.template """
Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
Click here to set your password and log in: <%= setNewPasswordUrl %>
If you have any questions or problems, please contact #{settings.adminEmail}
"""
compiledTemplate: _.template """
<p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
@ -19,14 +33,28 @@ templates.registered =
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
"""
templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
plainTextTemplate: _.template """
Hi <%= first_name %>,
I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
https://sharelatex.typeform.com/to/f5lBiZ
Thank you in advance.
Henry
ShareLaTeX Co-founder
"""
compiledTemplate: _.template '''
<p>Hi <%= first_name %>,</p>
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/F7OzIY">this survey</a>?</p>
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/f5lBiZ">this survey</a>?</p>
<p>Thank you in advance.</p>
@ -36,94 +64,112 @@ ShareLaTeX Co-founder
</p>
'''
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout
layout: BaseWithHeaderEmailLayout
type:"notification"
compiledTemplate: _.template """
<h2>Password Reset</h2>
<p>
plainTextTemplate: _.template """
Password Reset
We got a request to reset your #{settings.appName} password.
<p>
<center>
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
<div style="padding-right:10px;padding-left:10px">
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Arial;font-weight:bold;color:#fff;white-space:nowrap;display:block; text-align:center">
Reset password
</span>
</a>
</div>
</div>
</center>
Click this link to reset your password: <%= setNewPasswordUrl %>
If you ignore this message, your password won't be changed.
<p>
If you didn't request a password reset, let us know.
</p>
<p>Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
Thank you
templates.projectSharedWithYou =
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
layout: NotificationEmailLayout
type:"notification"
compiledTemplate: _.template """
<p>Hi, <%= owner.email %> wants to share <a href="<%= project.url %>">'<%= project.name %>'</a> with you</p>
<center>
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
<div style="padding-right:10px;padding-left:10px">
<a href="<%= project.url %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
View Project
</span>
</a>
</div>
</div>
</center>
<p> Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "Password Reset"
greeting: "Hi,"
message: "We got a request to reset your #{settings.appName} password."
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
ctaText: "Reset password"
ctaURL: opts.setNewPasswordUrl
gmailGoToAction: null
})
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: NotificationEmailLayout
subject: _.template "<%= project.name.slice(0, 40) %> - shared by <%= owner.email %>"
layout: BaseWithHeaderEmailLayout
type:"notification"
compiledTemplate: _.template """
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
<center>
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
View Project
</span>
</a>
</center>
<p> Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
plainTextTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
Follow this link to view the project: <%= inviteUrl %>
Thank you
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "#{ opts.project.name.slice(0, 40) } &ndash; shared by #{ opts.owner.email }"
greeting: "Hi,"
message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name.slice(0, 40) }&rdquo; with you."
secondaryMessage: null
ctaText: "View project"
ctaURL: opts.inviteUrl
gmailGoToAction:
target: opts.inviteUrl
name: "View project"
description: "Join #{ opts.project.name.slice(0, 40) } at ShareLaTeX"
})
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout
layout: BaseWithHeaderEmailLayout
type:"notification"
compiledTemplate: _.template """
<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p>
<center>
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;">
<div style="padding-right:10px;padding-left:10px">
<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
Verify now
</span>
</a>
</div>
</div>
</center>
<p> Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
plainTextTemplate: _.template """
Hi, please verify your email to join the <%= group_name %> and get your free premium account
Click this link to verify now: <%= completeJoinUrl %>
Thank You
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "Verify Email to join #{ opts.group_name } group"
greeting: "Hi,"
message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
secondaryMessage: null
ctaText: "Verify now"
ctaURL: opts.completeJoinUrl
gmailGoToAction: null
})
templates.testEmail =
subject: _.template "A Test Email from ShareLaTeX"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi,
This is a test email sent from ShareLaTeX.
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "A Test Email from ShareLaTeX"
greeting: "Hi,"
message: "This is a test email sent from ShareLaTeX"
secondaryMessage: null
ctaText: "Open ShareLaTeX"
ctaURL: "/"
gmailGoToAction: null
})
module.exports =
templates: templates
@ -137,5 +183,6 @@ module.exports =
return {
subject : template.subject(opts)
html: template.layout(opts)
text: template?.plainTextTemplate?(opts)
type:template.type
}

View file

@ -3,7 +3,7 @@ EmailBuilder = require "./EmailBuilder"
EmailSender = require "./EmailSender"
if !settings.email?
settings.email =
settings.email =
lifecycleEnabled:false
@ -14,6 +14,7 @@ module.exports =
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)
callback(err)

View file

@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport')
sgTransport = require('nodemailer-sendgrid-transport')
rateLimiter = require('../../infrastructure/RateLimiter')
_ = require("underscore")
if Settings.email? and Settings.email.fromAddress?
@ -25,7 +25,7 @@ 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?
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth")
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS")
logger.log "using smtp for email"
@ -39,21 +39,39 @@ if 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"
metrics.inc "email"
options =
to: options.to
from: defaultFromAddress
subject: options.subject
html: options.html
replyTo: options.replyTo || Settings.email.replyToAddress
socketTimeout: 30 * 1000
client.sendMail options, (err, res)->
checkCanSendEmail options, (err, canContinue)->
if err?
logger.err err:err, "error sending message"
else
logger.log "Message sent to #{options.to}"
callback(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"
else
logger.log "Message sent to #{options.to}"
callback(err)

View file

@ -0,0 +1,380 @@
_ = require("underscore")
settings = require "settings-sharelatex"
module.exports = _.template """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="Margin: 0; background: #f6f6f6 !important; margin: 0; min-height: 100%; padding: 0;">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width">
<title>Project invite</title>
<style>.avoid-auto-linking a,
.avoid-auto-linking a[href] {
color: #a93529 !important;
text-decoration: none !important;
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none; }
.avoid-auto-linking a:visited,
.avoid-auto-linking a[href]:visited {
color: #a93529; }
.avoid-auto-linking a:hover,
.avoid-auto-linking a[href]:hover {
color: #80281f; }
.avoid-auto-linking a:active,
.avoid-auto-linking a[href]:active {
color: #80281f; }
@media only screen {
html {
min-height: 100%;
background: #f6f6f6;
}
}
@media only screen and (max-width: 596px) {
.small-float-center {
margin: 0 auto !important;
float: none !important;
text-align: center !important;
}
.small-text-center {
text-align: center !important;
}
.small-text-left {
text-align: left !important;
}
.small-text-right {
text-align: right !important;
}
}
@media only screen and (max-width: 596px) {
.hide-for-large {
display: block !important;
width: auto !important;
overflow: visible !important;
max-height: none !important;
font-size: inherit !important;
line-height: inherit !important;
}
}
@media only screen and (max-width: 596px) {
table.body table.container .hide-for-large,
table.body table.container .row.hide-for-large {
display: table !important;
width: 100% !important;
}
}
@media only screen and (max-width: 596px) {
table.body table.container .callout-inner.hide-for-large {
display: table-cell !important;
width: 100% !important;
}
}
@media only screen and (max-width: 596px) {
table.body table.container .show-for-large {
display: none !important;
width: 0;
mso-hide: all;
overflow: hidden;
}
}
@media only screen and (max-width: 596px) {
table.body img {
width: auto;
height: auto;
}
table.body center {
min-width: 0 !important;
}
table.body .container {
width: 95% !important;
}
table.body .columns,
table.body .column {
height: auto !important;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding-left: 16px !important;
padding-right: 16px !important;
}
table.body .columns .column,
table.body .columns .columns,
table.body .column .column,
table.body .column .columns {
padding-left: 0 !important;
padding-right: 0 !important;
}
table.body .collapse .columns,
table.body .collapse .column {
padding-left: 0 !important;
padding-right: 0 !important;
}
td.small-1,
th.small-1 {
display: inline-block !important;
width: 8.33333% !important;
}
td.small-2,
th.small-2 {
display: inline-block !important;
width: 16.66667% !important;
}
td.small-3,
th.small-3 {
display: inline-block !important;
width: 25% !important;
}
td.small-4,
th.small-4 {
display: inline-block !important;
width: 33.33333% !important;
}
td.small-5,
th.small-5 {
display: inline-block !important;
width: 41.66667% !important;
}
td.small-6,
th.small-6 {
display: inline-block !important;
width: 50% !important;
}
td.small-7,
th.small-7 {
display: inline-block !important;
width: 58.33333% !important;
}
td.small-8,
th.small-8 {
display: inline-block !important;
width: 66.66667% !important;
}
td.small-9,
th.small-9 {
display: inline-block !important;
width: 75% !important;
}
td.small-10,
th.small-10 {
display: inline-block !important;
width: 83.33333% !important;
}
td.small-11,
th.small-11 {
display: inline-block !important;
width: 91.66667% !important;
}
td.small-12,
th.small-12 {
display: inline-block !important;
width: 100% !important;
}
.columns td.small-12,
.column td.small-12,
.columns th.small-12,
.column th.small-12 {
display: block !important;
width: 100% !important;
}
table.body td.small-offset-1,
table.body th.small-offset-1 {
margin-left: 8.33333% !important;
Margin-left: 8.33333% !important;
}
table.body td.small-offset-2,
table.body th.small-offset-2 {
margin-left: 16.66667% !important;
Margin-left: 16.66667% !important;
}
table.body td.small-offset-3,
table.body th.small-offset-3 {
margin-left: 25% !important;
Margin-left: 25% !important;
}
table.body td.small-offset-4,
table.body th.small-offset-4 {
margin-left: 33.33333% !important;
Margin-left: 33.33333% !important;
}
table.body td.small-offset-5,
table.body th.small-offset-5 {
margin-left: 41.66667% !important;
Margin-left: 41.66667% !important;
}
table.body td.small-offset-6,
table.body th.small-offset-6 {
margin-left: 50% !important;
Margin-left: 50% !important;
}
table.body td.small-offset-7,
table.body th.small-offset-7 {
margin-left: 58.33333% !important;
Margin-left: 58.33333% !important;
}
table.body td.small-offset-8,
table.body th.small-offset-8 {
margin-left: 66.66667% !important;
Margin-left: 66.66667% !important;
}
table.body td.small-offset-9,
table.body th.small-offset-9 {
margin-left: 75% !important;
Margin-left: 75% !important;
}
table.body td.small-offset-10,
table.body th.small-offset-10 {
margin-left: 83.33333% !important;
Margin-left: 83.33333% !important;
}
table.body td.small-offset-11,
table.body th.small-offset-11 {
margin-left: 91.66667% !important;
Margin-left: 91.66667% !important;
}
table.body table.columns td.expander,
table.body table.columns th.expander {
display: none !important;
}
table.body .right-text-pad,
table.body .text-pad-right {
padding-left: 10px !important;
}
table.body .left-text-pad,
table.body .text-pad-left {
padding-right: 10px !important;
}
table.menu {
width: 100% !important;
}
table.menu td,
table.menu th {
width: auto !important;
display: inline-block !important;
}
table.menu.vertical td,
table.menu.vertical th,
table.menu.small-vertical td,
table.menu.small-vertical th {
display: block !important;
}
table.menu[align="center"] {
width: auto !important;
}
table.button.small-expand,
table.button.small-expanded {
width: 100% !important;
}
table.button.small-expand table,
table.button.small-expanded table {
width: 100%;
}
table.button.small-expand table a,
table.button.small-expanded table a {
text-align: center !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
}
table.button.small-expand center,
table.button.small-expanded center {
min-width: 0;
}
}</style>
</head>
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="#F6F6F6" style="-moz-box-sizing: border-box; -ms-text-size-adjust: 100%; -webkit-box-sizing: border-box; -webkit-text-size-adjust: 100%; Margin: 0; background: #f6f6f6 !important; box-sizing: border-box; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; min-height: 100%; min-width: 100%; padding: 0; text-align: left; width: 100% !important;">
<!-- <span class="preheader"></span> -->
<table class="body" border="0" cellspacing="0" cellpadding="0" width="100%" height="100%" style="Margin: 0; background: #f6f6f6 !important; border-collapse: collapse; border-spacing: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; height: 100%; line-height: 1.3; margin: 0; min-height: 100%; padding: 0; text-align: left; vertical-align: top; width: 100%;">
<tr style="padding: 0; text-align: left; vertical-align: top;">
<td class="body-cell" align="center" valign="top" bgcolor="#F6F6F6" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; background: #f6f6f6 !important; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; padding-bottom: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
<center data-parsed="" style="min-width: 580px; width: 100%;">
<table align="center" class="wrapper header float-center" style="Margin: 0 auto; background: #fefefe; border-bottom: solid 1px #cfcfcf; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 20px; text-align: left; vertical-align: top; word-wrap: break-word;">
<table align="center" class="container" style="Margin: 0 auto; background: transparent; border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
<table class="row collapse" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 0; padding-left: 0; padding-right: 0; text-align: left; width: 588px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
<h1 class="sl-logotype" style="Margin: 0; Margin-bottom: 0; color: #333333; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 26px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 0; padding: 0; text-align: left; word-wrap: normal;">
<span>S</span><span class="sl-logotype-small" style="font-size: 80%;">HARE</span><span>L</span><span class="sl-logotype-small" style="font-size: 80%;">A</span><span>T</span><span class="sl-logotype-small" style="font-size: 80%;">E</span><span>X</span>
</h1>
</th>
<th class="expander" style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0 !important; text-align: left; visibility: hidden; width: 0;"></th></tr></table></th>
</tr></tbody></table>
</td></tr></tbody></table>
</td></tr></table>
<table class="spacer float-center" style="Margin: 0 auto; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; padding: 0; text-align: center; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<table align="center" class="container main float-center" style="Margin: 0 auto; Margin-top: 10px; background: #fefefe; border-collapse: collapse; border-spacing: 0; float: none; margin: 0 auto; margin-top: 10px; padding: 0; text-align: center; vertical-align: top; width: 580px;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<%= body %>
<table class="wrapper secondary" align="center" style="background: #f6f6f6; border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><td class="wrapper-inner" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; hyphens: auto; line-height: 1.3; margin: 0; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="10px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 10px; font-weight: normal; hyphens: auto; line-height: 10px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;"><small style="color: #7a7a7a; font-size: 80%;">
#{ settings.appName} &bull; <a href="#{ settings.siteUrl }" style="Margin: 0; color: #a93529; font-family: Helvetica, Arial, sans-serif; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; text-decoration: none;">#{ settings.siteUrl }</a>
</small></p>
</td></tr></table>
</td></tr></tbody></table>
</center>
</td>
</tr>
</table>
<!-- prevent Gmail on iOS font size manipulation -->
<div style="display:none; white-space:nowrap; font:15px courier; line-height:0;"> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </div>
</body>
</html>
"""

View file

@ -1,5 +1,6 @@
Errors = require "./Errors"
logger = require "logger-sharelatex"
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = ErrorController =
notFound: (req, res)->
@ -11,15 +12,16 @@ module.exports = ErrorController =
res.status(500)
res.render 'general/500',
title: "Server 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:req?.sesson?.user, "invalid csrf"
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
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
ErrorController.serverError req, res
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
ErrorController.serverError req, res

View file

@ -63,7 +63,8 @@ Reporter = (res) ->
res.contentType("application/json")
if failures.length > 0
res.send 500, JSON.stringify(results, null, 2)
logger.err failures:failures, "health check failed"
res.status(500).send(JSON.stringify(results, null, 2))
else
res.send 200, JSON.stringify(results, null, 2)
res.status(200).send(JSON.stringify(results, null, 2))

View file

@ -0,0 +1,20 @@
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = HistoryController =
proxyToHistoryApi: (req, res, next = (error) ->) ->
user_id = AuthenticationController.getLoggedInUserId req
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
next(error)

View file

@ -0,0 +1,28 @@
settings = require "settings-sharelatex"
request = require "request"
logger = require "logger-sharelatex"
module.exports = HistoryManager =
flushProject: (project_id, callback = (error) ->) ->
logger.log project_id: project_id, "flushing project in track-changes api"
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
request.post url, (error, res, body) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null)
else
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
callback(error)
archiveProject: (project_id, callback = ()->)->
logger.log project_id: project_id, "archving project in track-changes api"
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
request.post url, (error, res, body) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null)
else
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
callback(error)

View file

@ -5,8 +5,6 @@ DocstoreManager = require("../Docstore/DocstoreManager")
ProjectGetter = require("../Project/ProjectGetter")
ProjectUpdateHandler = require("../Project/ProjectUpdateHandler")
Project = require("../../models/Project").Project
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
MILISECONDS_IN_DAY = 86400000
module.exports = InactiveProjectManager =
@ -52,7 +50,6 @@ module.exports = InactiveProjectManager =
logger.log project_id:project_id, "deactivating inactive project"
jobs = [
(cb)-> DocstoreManager.archiveProject project_id, cb
# (cb)-> TrackChangesManager.archiveProject project_id, cb
(cb)-> ProjectUpdateHandler.markAsInactive project_id, cb
]
async.series jobs, (err)->

View file

@ -11,8 +11,8 @@ module.exports =
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, false, callback
logger.log user_id:user._id, key:@key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
@ -26,6 +26,6 @@ module.exports =
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, true, callback
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback

View file

@ -1,18 +1,20 @@
NotificationsHandler = require("./NotificationsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
_ = require("underscore")
module.exports =
getAllUnreadNotifications: (req, res)->
NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)->
unreadNotifications = _.map unreadNotifications, (notification)->
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 = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
notification_id = req.params.notification_id
NotificationsHandler.markAsRead user_id, notification_id, ->
res.send()

View file

@ -29,16 +29,15 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
forceCreate: true
}
if expiryDateTime?
payload.expires = expiryDateTime
if forceCreate
payload.forceCreate = true
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond

View file

@ -18,6 +18,8 @@ InactiveProjectManager = require("../InactiveData/InactiveProjectManager")
ProjectUpdateHandler = require("./ProjectUpdateHandler")
ProjectGetter = require("./ProjectGetter")
PrivilegeLevels = require("../Authorization/PrivilegeLevels")
AuthenticationController = require("../Authentication/AuthenticationController")
PackageVersions = require("../../infrastructure/PackageVersions")
module.exports = ProjectController =
@ -45,10 +47,10 @@ module.exports = ProjectController =
async.series jobs, (error) ->
return next(error) if error?
res.sendStatus(204)
updateProjectAdminSettings: (req, res, next) ->
project_id = req.params.Project_id
jobs = []
if req.body.publicAccessLevel?
jobs.push (callback) ->
@ -88,32 +90,33 @@ module.exports = ProjectController =
project_id = req.params.Project_id
projectName = req.body.projectName
logger.log project_id:project_id, projectName:projectName, "cloning project"
if !req.session.user?
if !AuthenticationController.isUserLoggedIn(req)
return res.send redir:"/register"
projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)->
currentUser = AuthenticationController.getSessionUser(req)
projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
if err?
logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project"
logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
return next(err)
res.send(project_id:project._id)
newProject: (req, res)->
user = req.session.user
user_id = AuthenticationController.getLoggedInUserId(req)
projectName = req.body.projectName?.trim()
template = req.body.template
logger.log user: user, projectType: template, name: projectName, "creating project"
logger.log user: user_id, projectType: template, name: projectName, "creating project"
async.waterfall [
(cb)->
if template == 'example'
projectCreationHandler.createExampleProject user._id, projectName, cb
projectCreationHandler.createExampleProject user_id, projectName, cb
else
projectCreationHandler.createBasicProject user._id, projectName, cb
projectCreationHandler.createBasicProject user_id, projectName, cb
], (err, project)->
if err?
logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project"
logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
res.sendStatus 500
else
logger.log project: project, user: user, name: projectName, templateType: template, "created project"
logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
res.send {project_id:project._id}
@ -131,7 +134,8 @@ module.exports = ProjectController =
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
currentUser = AuthenticationController.getSessionUser(req)
async.parallel {
tags: (cb)->
TagsHandler.getAllTags user_id, cb
@ -140,7 +144,7 @@ module.exports = ProjectController =
projects: (cb)->
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
user: (cb) ->
User.findById user_id, "featureSwitches", cb
}, (err, results)->
@ -149,7 +153,9 @@ module.exports = ProjectController =
return next(err)
logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0]
notifications = require("underscore").map results.notifications, (notification)->
notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
@ -183,19 +189,19 @@ module.exports = ProjectController =
if !Settings.editorIsOpen
return res.render("general/closed", {title:"updating_site"})
if req.session.user?
user_id = req.session.user._id
if AuthenticationController.isUserLoggedIn(req)
user_id = AuthenticationController.getLoggedInUserId(req)
anonymous = false
else
anonymous = true
user_id = null
project_id = req.params.Project_id
logger.log project_id:project_id, "loading editor"
logger.log project_id:project_id, anonymous:anonymous, user_id:user_id, "loading editor"
async.parallel {
project: (cb)->
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb
ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1, track_changes: 1 }, cb
user: (cb)->
if !user_id?
cb null, defaultSettingsForAnonymousUser(user_id)
@ -259,7 +265,9 @@ module.exports = ProjectController =
fontSize : user.ace.fontSize
autoComplete: user.ace.autoComplete
pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation
}
trackChangesEnabled: !!project.track_changes
privilegeLevel: privilegeLevel
chatUrl: Settings.apis.chat.url
anonymous: anonymous
@ -320,6 +328,7 @@ defaultSettingsForAnonymousUser = (user_id)->
autoComplete: true
spellCheckLanguage: ""
pdfViewer: ""
syntaxValidation: true
subscription:
freeTrial:
allowed: true
@ -328,8 +337,8 @@ defaultSettingsForAnonymousUser = (user_id)->
THEME_LIST = []
do generateThemeList = () ->
files = fs.readdirSync __dirname + '/../../../../public/js/ace'
files = fs.readdirSync __dirname + '/../../../../public/js/' + PackageVersions.lib('ace')
for file in files
if file.slice(-2) == "js" and file.match(/^theme-/)
cleanName = file.slice(0,-3).slice(6)
THEME_LIST.push cleanName
THEME_LIST.push cleanName

View file

@ -19,6 +19,11 @@ module.exports = ProjectEditorHandler =
if !result.invites?
result.invites = []
hasTrackChanges = false
for member in members
if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes
hasTrackChanges = true
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
@ -32,6 +37,7 @@ module.exports = ProjectEditorHandler =
compileGroup:"standard"
templates: false
references: false
trackChanges: hasTrackChanges
})
return result

View file

@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler =
doc = new Doc name: docName
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
# which hasn't been created in docstore.
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) ->
return callback(err) if err?
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler =
return callback(err)
callback(err, folder, parentFolder_id)
updateDocLines : (project_id, doc_id, lines, callback = (error) ->)->
updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
return callback(err) if err?
return callback(new Errors.NotFoundError("project not found")) if !project?
@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler =
return callback(error)
logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc"
DocstoreManager.updateDoc project_id, doc_id, lines, (err, modified, rev) ->
DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) ->
if err?
logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore"
return callback(err)

View file

@ -81,7 +81,7 @@ module.exports = ProjectLocator =
else
getRootDoc project
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity, type)->)->
getParentFolder = (haystackFolder, foldersList, level, cb)->
if foldersList.length == 0
@ -100,12 +100,22 @@ module.exports = ProjectLocator =
getEntity = (folder, entityName, cb)->
if !entityName?
return cb null, folder
enteties = _.union folder.fileRefs, folder.docs, folder.folders
result = _.find enteties, (entity)->
entity?.name.toLowerCase() == entityName.toLowerCase()
return cb null, folder, "folder"
for file in folder.fileRefs or []
if file?.name.toLowerCase() == entityName.toLowerCase()
result = file
type = "file"
for doc in folder.docs or []
if doc?.name.toLowerCase() == entityName.toLowerCase()
result = doc
type = "doc"
for childFolder in folder.folders or []
if childFolder?.name.toLowerCase() == entityName.toLowerCase()
result = childFolder
type = "folder"
if result?
cb null, result
cb null, result, type
else
cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found")
@ -117,7 +127,7 @@ module.exports = ProjectLocator =
if !project?
return callback("project could not be found for finding a element #{project_or_id}")
if needlePath == '' || needlePath == '/'
return callback(null, project.rootFolder[0])
return callback(null, project.rootFolder[0], "folder")
if needlePath.indexOf('/') == 0
needlePath = needlePath.substring(1)

View file

@ -1,9 +1,11 @@
logger = require('logger-sharelatex')
ReferalHandler = require('./ReferalHandler')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
module.exports =
bonus: (req, res)->
ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)->
user_id = AuthenticationController.getLoggedInUserId(req)
ReferalHandler.getReferedUserIds user_id, (err, refered_users)->
res.render "referal/bonus",
title: "bonus_please_recommend_us"
refered_users: refered_users

View file

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

View file

@ -1,32 +1,33 @@
RateLimiter = require "../../infrastructure/RateLimiter"
logger = require "logger-sharelatex"
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RateLimiterMiddlewear =
###
Do not allow more than opts.maxRequests from a single client in
opts.timeInterval. Pass an array of opts.params to segment this based on
parameters in the request URL, e.g.:
app.get "/project/:project_id", RateLimiterMiddlewear.rateLimit(endpointName: "open-editor", params: ["project_id"])
will rate limit each project_id separately.
Unique clients are identified by user_id if logged in, and IP address if not.
###
rateLimit: (opts) ->
return (req, res, next) ->
if req.session?.user?
user_id = req.session.user._id
else
user_id = req.ip
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
params = (opts.params or []).map (p) -> req.params[p]
params.push user_id
subjectName = params.join(":")
if opts.ipOnly
subjectName = req.ip
if !opts.endpointName?
throw new Error("no endpointName provided")
options = {
endpointName: opts.endpointName
timeInterval: opts.timeInterval or 60
subjectName: params.join(":")
subjectName: subjectName
throttle: opts.maxRequests or 6
}
RateLimiter.addCount options, (error, canContinue)->
@ -37,4 +38,4 @@ module.exports = RateLimiterMiddlewear =
logger.warn options, "rate limit exceeded"
res.status(429) # Too many requests
res.write("Rate limit reached, please try again later")
res.end()
res.end()

View file

@ -1,26 +0,0 @@
Settings = require("settings-sharelatex")
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
crypto = require("crypto")
async = require("async")
module.exports =
_getEmailKey : (email)->
hash = crypto.createHash("md5").update(email).digest("hex")
return "e_sess:#{hash}"
tracksession:(sessionId, email, callback = ->)->
session_lookup_key = @_getEmailKey(email)
rclient.set session_lookup_key, sessionId, callback
invalidateSession:(email, callback = ->)->
session_lookup_key = @_getEmailKey(email)
rclient.get session_lookup_key, (err, sessionId)->
async.series [
(cb)-> rclient.del sessionId, cb
(cb)-> rclient.del session_lookup_key, cb
], callback

View file

@ -1,13 +1,15 @@
request = require 'request'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
AuthenticationController = require('../Authentication/AuthenticationController')
TEN_SECONDS = 1000 * 10
module.exports = SpellingController =
proxyRequestToSpellingApi: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
url = req.url.slice("/spelling".length)
url = "/user/#{req.session.user._id}#{url}"
url = "/user/#{user_id}#{url}"
req.headers["Host"] = Settings.apis.spelling.host
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
.on "error", (error) ->

View file

@ -5,12 +5,13 @@ Path = require "path"
fs = require "fs"
ErrorController = require "../Errors/ErrorController"
AuthenticationController = require('../Authentication/AuthenticationController')
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
module.exports = HomeController =
index : (req,res)->
if req.session.user
if AuthenticationController.isUserLoggedIn(req)
if req.query.scribtex_path?
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
else
@ -33,4 +34,4 @@ module.exports = HomeController =
res.render "external/#{page}.jade",
title: title
else
ErrorController.notFound(req, res, next)
ErrorController.notFound(req, res, next)

View file

@ -1,75 +1,16 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
_ = require("underscore")
ErrorController = require "../Errors/ErrorController"
StaticPageHelpers = require("./StaticPageHelpers")
sanitize = require('sanitizer')
Settings = require("settings-sharelatex")
contentful = require('contentful')
marked = require("marked")
sixpack = require("../../infrastructure/Sixpack")
module.exports = UniversityController =
module.exports = UniversityController =
getPage: (req, res, next)->
url = req.url?.toLowerCase()
universityUrl = "#{settings.apis.university.url}#{url}"
if StaticPageHelpers.shouldProxy(url)
return UniversityController._directProxy universityUrl, res
logger.log url:url, "proxying request to university api"
request.get universityUrl, (err, r, data)->
if r?.statusCode == 404
return UniversityController.getContentfulPage(req, res, next)
if err?
return res.send 500
data = data.trim()
try
data = JSON.parse(data)
data.content = data.content.replace(/__ref__/g, sanitize.escape(req.query.ref))
catch err
logger.err err:err, data:data, "error parsing data from data"
res.render "university/university_holder", data
url = req.url?.toLowerCase().replace(".html","")
return res.redirect("/i#{url}")
getIndexPage: (req, res)->
client = sixpack.client(req?.session?.user?._id?.toString() || req.ip)
client.participate 'instapage-pages', ['default', 'instapage'], (err, response)->
if response?.alternative?.name == "instapage"
return res.redirect("/i/university")
else
req.url = "/university/index.html"
UniversityController.getPage req, res
_directProxy: (originUrl, res)->
upstream = request.get(originUrl)
upstream.on "error", (error) ->
logger.error err: error, "university proxy error"
upstream.pipe res
getContentfulPage: (req, res, next)->
console.log Settings.contentful
if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken?
return ErrorController.notFound(req, res, next)
client = contentful.createClient({
space: Settings.contentful?.uni?.space
accessToken: Settings.contentful?.uni?.accessToken
})
url = req.url?.toLowerCase().replace("/university/","")
client.getEntries({content_type: 'caseStudy', 'fields.slug':url})
.catch (e)->
return res.send 500
.then (entry)->
if !entry? or !entry.items? or entry.items.length == 0
return ErrorController.notFound(req, res, next)
viewData = entry.items[0].fields
viewData.html = marked(viewData.content)
res.render "university/case_study", viewData:viewData
return res.redirect("/i/university")

View file

@ -418,7 +418,15 @@ module.exports = RecurlyWrapper =
url: "subscriptions/#{subscriptionId}/cancel",
method: "put"
}, (error, response, body) ->
callback(error)
if error?
RecurlyWrapper._parseXml body, (_err, parsed) ->
if parsed?.error?.description == "A canceled subscription can't transition to canceled"
logger.log {subscriptionId, error, body}, "subscription already cancelled, not really an error, proceeding"
callback(null)
else
callback(error)
else
callback(null)
)
reactivateSubscription: (subscriptionId, callback) ->

View file

@ -8,192 +8,190 @@ Settings = require 'settings-sharelatex'
logger = require('logger-sharelatex')
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
UserGetter = require "../User/UserGetter"
module.exports = SubscriptionController =
plansPage: (req, res, next) ->
plans = SubscriptionViewModelBuilder.buildViewModel()
if !req.session.user?
baseUrl = "/register?redir="
else
baseUrl = ""
viewName = "subscriptions/plans"
if req.query.v?
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
currentUser = null
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
return next(err) if err?
res.render viewName,
title: "plans_and_pricing"
plans: plans
baseUrl: baseUrl
gaExperiments: Settings.gaExperiments.plansPage
recomendedCurrency:recomendedCurrency
render = () ->
res.render viewName,
title: "plans_and_pricing"
plans: plans
gaExperiments: Settings.gaExperiments.plansPage
recomendedCurrency:recomendedCurrency
shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
user_id = AuthenticationController.getLoggedInUserId(req)
if user_id?
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
return next(err) if err?
currentUser = user
render()
else
render()
#get to show the recurly.js page
paymentPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if hasSubscription or !plan?
res.redirect "/user/subscription"
else
currency = req.query.currency?.toUpperCase()
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
return next(err) if err?
if recomendedCurrency? and !currency?
currency = recomendedCurrency
RecurlyWrapper.sign {
subscription:
plan_code : req.query.planCode
user = AuthenticationController.getSessionUser(req)
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if hasSubscription or !plan?
res.redirect "/user/subscription"
else
currency = req.query.currency?.toUpperCase()
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
return next(err) if err?
if recomendedCurrency? and !currency?
currency = recomendedCurrency
RecurlyWrapper.sign {
subscription:
plan_code : req.query.planCode
currency: currency
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/new",
title : "subscribe"
plan_code: req.query.planCode
currency: currency
countryCode:countryCode
plan:plan
showStudentPlan: req.query.ssp
recurlyConfig: JSON.stringify
currency: currency
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/new",
title : "subscribe"
plan_code: req.query.planCode
currency: currency
countryCode:countryCode
plan:plan
showStudentPlan: req.query.ssp
recurlyConfig: JSON.stringify
currency: currency
subdomain: Settings.apis.recurly.subdomain
showCouponField: req.query.scf
showVatField: req.query.svf
couponCode: req.query.cc or ""
subdomain: Settings.apis.recurly.subdomain
showCouponField: req.query.scf
showVatField: req.query.svf
couponCode: req.query.cc or ""
userSubscriptionPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
userCustomSubscriptionPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
return next(err)
res.render "subscriptions/custom_account",
title: "your_subscription"
subscription: subscription
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
return next(err)
res.render "subscriptions/custom_account",
title: "your_subscription"
subscription: subscription
editBillingDetailsPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return callback(error) if error?
recurly_token_id = req.body.recurly_token_id
subscriptionDetails = req.body.subscriptionDetails
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
return res.sendStatus 500
res.sendStatus 201
user = AuthenticationController.getSessionUser(req)
recurly_token_id = req.body.recurly_token_id
subscriptionDetails = req.body.subscriptionDetails
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
return res.sendStatus 500
res.sendStatus 201
successful_subscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) =>
user = AuthenticationController.getSessionUser(req)
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
cancelSubscription: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
logger.log user_id:user._id, "canceling subscription"
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
return next(err)
res.redirect "/user/subscription"
user = AuthenticationController.getSessionUser(req)
logger.log user_id:user._id, "canceling subscription"
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
return next(err)
res.redirect "/user/subscription"
updateSubscription: (req, res, next)->
_origin = req?.query?.origin || null
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
if !planCode?
err = new Error('plan_code is not defined')
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
user = AuthenticationController.getSessionUser(req)
planCode = req.body.plan_code
if !planCode?
err = new Error('plan_code is not defined')
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
return next(err)
res.redirect "/user/subscription"
res.redirect "/user/subscription"
reactivateSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
logger.log user_id:user._id, "reactivating subscription"
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
return next(err)
res.redirect "/user/subscription"
user = AuthenticationController.getSessionUser(req)
logger.log user_id:user._id, "reactivating subscription"
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
return next(err)
res.redirect "/user/subscription"
recurlyCallback: (req, res, next)->
logger.log data: req.body, "received recurly callback"
@ -207,47 +205,44 @@ module.exports = SubscriptionController =
res.sendStatus 200
renderUpgradeToAnnualPlanPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
else if planCode?.indexOf("student") != -1
planName = "student"
else if planCode?.indexOf("collaborator") != -1
planName = "collaborator"
if !hasSubscription
return res.redirect("/user/subscription/plans")
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
res.render "subscriptions/upgradeToAnnual",
title: "Upgrade to annual"
planName: planName
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
else if planCode?.indexOf("student") != -1
planName = "student"
else if planCode?.indexOf("collaborator") != -1
planName = "collaborator"
if !hasSubscription
return res.redirect("/user/subscription/plans")
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
res.render "subscriptions/upgradeToAnnual",
title: "Upgrade to annual"
planName: planName
processUpgradeToAnnualPlan: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
return next(err)
res.sendStatus 200
user = AuthenticationController.getSessionUser(req)
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
return next(err)
res.sendStatus 200
extendTrial: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500
else
res.send 200
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500
else
res.send 200
recurlyNotificationParser: (req, res, next) ->
xml = ""

View file

@ -3,27 +3,28 @@ logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator")
ErrorsController = require("../Errors/ErrorController")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
AuthenticationController = require('../Authentication/AuthenticationController')
_ = require("underscore")
async = require("async")
module.exports =
addUserToGroup: (req, res)->
adminUserId = req.session.user._id
adminUserId = AuthenticationController.getLoggedInUserId(req)
newEmail = req.body?.email?.toLowerCase()?.trim()
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
if err?
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
return res.sendStatus 500
result =
result =
user:user
if err and err.limitReached
result.limitReached = true
res.json(result)
removeUserFromGroup: (req, res)->
adminUserId = req.session.user._id
adminUserId = AuthenticationController.getLoggedInUserId(req)
userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
@ -31,10 +32,10 @@ module.exports =
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
return res.sendStatus 500
res.send()
removeSelfFromGroup: (req, res)->
adminUserId = req.query.admin_user_id
userToRemove_id = req.session.user._id
userToRemove_id = AuthenticationController.getLoggedInUserId(req)
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
if err?
@ -43,7 +44,7 @@ module.exports =
res.send()
renderSubscriptionGroupAdminPage: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
return res.redirect("/")
@ -55,11 +56,11 @@ module.exports =
renderGroupInvitePage: (req, res)->
group_subscription_id = req.params.subscription_id
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id)
if !licence?
return ErrorsController.notFound(req, res)
jobs =
jobs =
partOfGroup: (cb)->
SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb
subscription: (cb)->
@ -77,22 +78,26 @@ module.exports =
beginJoinGroup: (req, res)->
subscription_id = req.params.subscription_id
user_id = req.session.user._id
currentUser = AuthenticationController.getSessionUser(req)
if !currentUser?
logger.err {subscription_id}, "error getting current user"
return res.sendStatus 500
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
if !licence?
return ErrorsController.notFound(req, res)
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)->
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
if err?
res.sendStatus 500
else
res.sendStatus 200
completeJoin: (req, res)->
currentUser = AuthenticationController.getSessionUser(req)
subscription_id = req.params.subscription_id
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
return ErrorsController.notFound(req, res)
email = req?.session?.user?.email
logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group"
email = currentUser?.email
logger.log subscription_id:subscription_id, user_id:currentUser?._id, email:email, "starting the completion of joining group"
SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)->
if err? and err == "token_not_found"
return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
@ -109,10 +114,10 @@ module.exports =
return ErrorsController.notFound(req, res)
res.render "subscriptions/group/successful_join",
title: "Sucessfully joined group"
licenceName:licence.name
licenceName:licence.name
exportGroupCsv: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "exporting group csv"
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan

View file

@ -7,6 +7,8 @@ SubscriptionUpdater = require("./SubscriptionUpdater")
LimitationsManager = require('./LimitationsManager')
EmailHandler = require("../Email/EmailHandler")
Events = require "../../infrastructure/Events"
Analytics = require("../Analytics/AnalyticsManager")
module.exports =
@ -52,6 +54,7 @@ module.exports =
setTimeout (-> EmailHandler.sendEmail "canceledSubscription", emailOpts
), ONE_HOUR_IN_MS
Events.emit "cancelSubscription", user._id
Analytics.recordEvent user._id, "subscription-canceled"
callback()
else
callback()

View file

@ -14,7 +14,11 @@ module.exports =
logger.log user_id:user_id, "got users subscription"
callback(err, subscription)
getMemberSubscriptions: (user_id, callback) ->
getMemberSubscriptions: (user_or_id, callback) ->
if user_or_id? and user_or_id._id?
user_id = user_or_id._id
else if user_or_id?
user_id = user_or_id
logger.log user_id: user_id, "getting users group subscriptions"
Subscription.find(member_ids: user_id).populate("admin_id").exec callback
@ -25,4 +29,4 @@ module.exports =
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
getGroupSubscriptionMemberOf: (user_id, callback)->
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback

View file

@ -4,6 +4,7 @@ PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
LimitationsManager = require("./LimitationsManager")
SubscriptionLocator = require("./SubscriptionLocator")
logger = require('logger-sharelatex')
_ = require("underscore")
module.exports =
@ -16,6 +17,10 @@ module.exports =
if subscription?
return callback(error) if error?
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
if !plan?
err = new Error("No plan found for planCode '#{subscription.planCode}'")
logger.error {user_id: user._id, err}, "error getting subscription plan for user"
return callback(err)
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
tax = recurlySubscription?.tax_in_cents || 0
callback null, {
@ -39,7 +44,7 @@ module.exports =
allPlans = {}
plans.forEach (plan)->
allPlans[plan.planCode] = plan
result =
allPlans: allPlans
@ -49,7 +54,7 @@ module.exports =
result.studentAccounts = _.filter plans, (plan)->
plan.planCode.indexOf("student") != -1
result.groupMonthlyPlans = _.filter plans, (plan)->
plan.groupPlan and !plan.annual
@ -63,4 +68,3 @@ module.exports =
!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
return result

View file

@ -1,48 +1,49 @@
TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
getAllTags: (req, res, next)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "getting tags"
TagsHandler.getAllTags user_id, (error, allTags)->
return next(error) if error?
res.json(allTags)
createTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
name = req.body.name
logger.log {user_id, name}, "creating tag"
TagsHandler.createTag user_id, name, (error, tag) ->
return next(error) if error?
res.json(tag)
addProjectToTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "adding tag to project"
TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
removeProjectFromTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "removing tag from project"
TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
deleteTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
logger.log {user_id, tag_id}, "deleting tag"
TagsHandler.deleteTag user_id, tag_id, (error) ->
return next(error) if error?
res.status(204).end()
renameTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
name = req.body?.name
if !name?

View file

@ -33,16 +33,11 @@ module.exports =
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
deleteUpdate: (project_id, path, source, callback)->
projectLocator.findElementByPath project_id, path, (err, element)->
type = 'file'
projectLocator.findElementByPath project_id, path, (err, element, type)->
if err? || !element?
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback()
if element.lines?
type = 'doc'
else if element.folders?
type = 'folder'
logger.log project_id:project_id, updateType:path, updateType:type, element:element, "processing update to delete entity from tpds"
logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
editorController.deleteEntity project_id, element._id, type, source, (err)->
logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds"
callback()
@ -56,12 +51,13 @@ module.exports =
return callback(err)
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds"
if doc_id?
editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)->
callback()
editorController.setDoc project_id, doc_id, user_id, docLines, source, callback
else
setupNewEntity project_id, path, (err, folder, fileName)->
editorController.addDoc project_id, folder._id, fileName, docLines, source, (err)->
callback()
if err?
logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file"
return callback(err)
editorController.addDoc project_id, folder._id, fileName, docLines, source, callback
processFile: (project_id, file_id, fsPath, path, source, callback)->
finish = (err)->
@ -69,10 +65,13 @@ module.exports =
callback(err)
logger.log project_id:project_id, file_id:file_id, path:path, "processing file update from tpds"
setupNewEntity project_id, path, (err, folder, fileName) =>
if file_id?
if err?
logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file"
return callback(err)
else if file_id?
editorController.replaceFile project_id, file_id, fsPath, source, finish
else
editorController.addFile project_id, folder._id, fileName, fsPath, source, finish
editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish
writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
if !file_id?

View file

@ -0,0 +1,23 @@
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
DocstoreManager = require "../Docstore/DocstoreManager"
UserInfoManager = require "../User/UserInfoManager"
async = require "async"
module.exports = RangesManager =
getAllRanges: (project_id, callback = (error, docs) ->) ->
DocumentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
DocstoreManager.getAllRanges project_id, callback
getAllChangesUsers: (project_id, callback = (error, users) ->) ->
user_ids = {}
RangesManager.getAllRanges project_id, (error, docs) ->
return callback(error) if error?
jobs = []
for doc in docs
for change in doc.ranges?.changes or []
user_ids[change.metadata.user_id] = true
async.mapSeries Object.keys(user_ids), (user_id, cb) ->
UserInfoManager.getPersonalInfo user_id, cb
, callback

View file

@ -1,21 +1,42 @@
RangesManager = require "./RangesManager"
logger = require "logger-sharelatex"
request = require "request"
settings = require "settings-sharelatex"
AuthenticationController = require "../Authentication/AuthenticationController"
UserInfoController = require "../User/UserInfoController"
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
TrackChangesManager = require "./TrackChangesManager"
module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
getAllRanges: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "request for project ranges"
RangesManager.getAllRanges project_id, (error, docs = []) ->
return next(error) if error?
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
next(error)
docs = ({id: d._id, ranges: d.ranges} for d in docs)
res.json docs
getAllChangesUsers: (req, res, next) ->
project_id = req.params.project_id
logger.log {project_id}, "request for project range users"
RangesManager.getAllChangesUsers project_id, (error, users) ->
return next(error) if error?
users = (UserInfoController.formatPersonalInfo(user) for user in users)
# Get rid of any anonymous/deleted user objects
users = users.filter (u) -> u?.id?
res.json users
acceptChange: (req, res, next) ->
{project_id, doc_id, change_id} = req.params
logger.log {project_id, doc_id, change_id}, "request to accept change"
DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)->
res.send 204
toggleTrackChanges: (req, res, next) ->
{project_id} = req.params
track_changes_on = !!req.body.on
logger.log {project_id, track_changes_on}, "request to toggle track changes"
TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)->
res.send 204

View file

@ -1,28 +1,5 @@
settings = require "settings-sharelatex"
request = require "request"
logger = require "logger-sharelatex"
Project = require("../../models/Project").Project
module.exports = TrackChangesManager =
flushProject: (project_id, callback = (error) ->) ->
logger.log project_id: project_id, "flushing project in track-changes api"
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush"
request.post url, (error, res, body) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null)
else
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
logger.error err: error, project_id: project_id, "error flushing project in track-changes api"
callback(error)
archiveProject: (project_id, callback = ()->)->
logger.log project_id: project_id, "archving project in track-changes api"
url = "#{settings.apis.trackchanges.url}/project/#{project_id}/archive"
request.post url, (error, res, body) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null)
else
error = new Error("track-changes api responded with non-success code: #{res.statusCode} #{url}")
logger.error err: error, project_id: project_id, "error archving project in track-changes api"
callback(error)
toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
Project.update {_id: project_id}, {track_changes: track_changes_on}, callback

View file

@ -4,11 +4,12 @@ fs = require "fs"
Path = require "path"
FileSystemImportManager = require "./FileSystemImportManager"
ProjectUploadManager = require "./ProjectUploadManager"
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = ProjectUploadController =
uploadProject: (req, res, next) ->
timer = new metrics.Timer("project-upload")
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{originalname, path} = req.files.qqfile
name = Path.basename(originalname, ".zip")
ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
@ -24,7 +25,7 @@ module.exports = ProjectUploadController =
project: project._id, file_path: path, file_name: name,
"uploaded project"
res.send success: true, project_id: project._id
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
name = req.files.qqfile?.originalname
@ -35,7 +36,7 @@ module.exports = ProjectUploadController =
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
return res.send success: false
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
fs.unlink path, ->
timer.done()
@ -50,6 +51,3 @@ module.exports = ProjectUploadController =
project_id: project_id, file_path: path, file_name: name, folder_id: folder_id
"uploaded file"
res.send success: true, entity_id: entity?._id

View file

@ -8,27 +8,50 @@ logger = require("logger-sharelatex")
metrics = require("../../infrastructure/Metrics")
Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager")
AuthenticationController = require('../Authentication/AuthenticationController')
UserSessionsManager = require("./UserSessionsManager")
UserUpdater = require("./UserUpdater")
settings = require "settings-sharelatex"
module.exports = UserController =
deleteUser: (req, res)->
user_id = req.session.user._id
UserDeleter.deleteUser user_id, (err)->
if !err?
req.session?.destroy()
res.sendStatus(200)
tryDeleteUser: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
password = req.body.password
logger.log {user_id}, "trying to delete user account"
if !password? or password == ''
logger.err {user_id}, 'no password supplied for attempt to delete account'
return res.sendStatus(403)
AuthenticationManager.authenticate {_id: user_id}, password, (err, user) ->
if err?
logger.err {user_id}, 'error authenticating during attempt to delete account'
return next(err)
if !user
logger.err {user_id}, 'auth failed during attempt to delete account'
return res.sendStatus(403)
UserDeleter.deleteUser user_id, (err) ->
if err?
logger.err {user_id}, "error while deleting user account"
return next(err)
sessionId = req.sessionID
req.logout?()
req.session.destroy (err) ->
if err?
logger.err err: err, 'error destorying session'
return next(err)
UserSessionsManager.untrackSession(user, sessionId)
res.sendStatus(200)
unsubscribe: (req, res)->
UserLocator.findById req.session.user._id, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
UserLocator.findById user_id, (err, user)->
newsLetterManager.unsubscribe user, ->
res.send()
updateUserSettings : (req, res)->
logger.log user: req.session.user, "updating account settings"
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
usingExternalAuth = settings.ldap? or settings.saml?
logger.log user_id: user_id, "updating account settings"
User.findById user_id, (err, user)->
if err? or !user?
logger.err err:err, user_id:user_id, "problem updaing user settings"
@ -54,13 +77,19 @@ module.exports = UserController =
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
if req.body.pdfViewer?
user.ace.pdfViewer = req.body.pdfViewer
if req.body.syntaxValidation?
user.ace.syntaxValidation = req.body.syntaxValidation
user.save (err)->
newEmail = req.body.email?.trim().toLowerCase()
if !newEmail? or newEmail == user.email
if !newEmail? or newEmail == user.email or usingExternalAuth
# end here, don't update email
AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name})
return res.sendStatus 200
else if newEmail.indexOf("@") == -1
# email invalid
return res.sendStatus(400)
else
# update the user email
UserUpdater.changeEmailAddress user_id, newEmail, (err)->
if err?
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
@ -73,7 +102,7 @@ module.exports = UserController =
if err?
logger.err err:err, user_id:user_id, "error getting user for email update"
return res.send 500
req.session.user.email = user.email
AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name})
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
if err?
logger.err err:err, "error populateGroupLicenceInvite"
@ -81,9 +110,10 @@ module.exports = UserController =
logout : (req, res)->
metrics.inc "user.logout"
logger.log user: req?.session?.user, "logging out"
user = AuthenticationController.getSessionUser(req)
logger.log user: user, "logging out"
sessionId = req.sessionID
user = req?.session?.user
req.logout?() # passport logout
req.session.destroy (err)->
if err
logger.err err: err, 'error destorying session'
@ -102,13 +132,22 @@ module.exports = UserController =
setNewPasswordUrl: setNewPasswordUrl
}
clearSessions: (req, res, next = (error) ->) ->
metrics.inc "user.clear-sessions"
user = AuthenticationController.getSessionUser(req)
logger.log {user_id: user._id}, "clearing sessions for user"
UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
return next(err) if err?
res.sendStatus 201
changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change"
oldPass = req.body.currentPassword
AuthenticationManager.authenticate {_id:req.session.user._id}, oldPass, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)->
return next(err) if err?
if(user)
logger.log user: req.session.user, "changing password"
logger.log user: user._id, "changing password"
newPassword1 = req.body.newPassword1
newPassword2 = req.body.newPassword2
if newPassword1 != newPassword2
@ -128,7 +167,7 @@ module.exports = UserController =
type:'success'
text:'Your password has been changed'
else
logger.log user: user, "current password wrong"
logger.log user_id: user_id, "current password wrong"
res.send
message:
type:'error'

View file

@ -17,6 +17,7 @@ module.exports =
user = new User()
user.email = opts.email
user.holdingAccount = opts.holdingAccount
user.ace.syntaxValidation = true
username = opts.email.match(/^[^@]*/)
if opts.first_name? and opts.first_name.length != 0

View file

@ -3,12 +3,14 @@ logger = require("logger-sharelatex")
UserDeleter = require("./UserDeleter")
UserUpdater = require("./UserUpdater")
sanitize = require('sanitizer')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = UserController =
getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) ->
logger.log user: req.user, "reciving request for getting logged in users personal info"
return next(new Error("User is not logged in")) if !req.user?
UserGetter.getUser req.user._id, {
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "reciving request for getting logged in users personal info"
return next(new Error("User is not logged in")) if !user_id?
UserGetter.getUser user_id, {
first_name: true, last_name: true,
role:true, institution:true,
email: true, signUpDate: true
@ -24,20 +26,14 @@ module.exports = UserController =
UserController.sendFormattedPersonalInfo(user, res, next)
sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
UserController._formatPersonalInfo user, (error, info) ->
return next(error) if error?
res.send JSON.stringify(info)
info = UserController.formatPersonalInfo(user)
res.send JSON.stringify(info)
_formatPersonalInfo: (user, callback = (error, info) ->) ->
callback null, {
id: user._id.toString()
first_name: user.first_name
last_name: user.last_name
email: user.email
signUpDate: user.signUpDate
role: user.role
institution: user.institution
}
formatPersonalInfo: (user, callback = (error, info) ->) ->
if !user?
return {}
formatted_user = { id: user._id.toString() }
for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
if user[key]?
formatted_user[key] = user[key]
return formatted_user

View file

@ -0,0 +1,5 @@
UserGetter = require "./UserGetter"
module.exports = UserInfoManager =
getPersonalInfo: (user_id, callback = (error) ->) ->
UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback

View file

@ -1,9 +1,11 @@
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
UserSessionsManager = require("./UserSessionsManager")
ErrorController = require("../Errors/ErrorController")
logger = require("logger-sharelatex")
Settings = require("settings-sharelatex")
fs = require('fs')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
@ -18,18 +20,17 @@ module.exports =
res.render 'user/register',
title: 'register'
redir: req.query.redir
sharedProjectData: sharedProjectData
newTemplateData: newTemplateData
new_email:req.query.new_email || ""
activateAccountPage: (req, res) ->
# An 'activation' is actually just a password reset on an account that
# was set with a random password originally.
logger.log query:req.query, "activiate account page called"
if !req.query?.user_id? or !req.query?.token?
return ErrorController.notFound(req, res)
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
return next(error) if error?
if !user
@ -47,17 +48,35 @@ module.exports =
token: req.query.token
loginPage : (req, res)->
# if user is being sent to /login with explicit redirect (redir=/foo),
# such as being sent from the editor to /login, then set the redirect explicitly
if req.query.redir? and !AuthenticationController._getRedirectFromSession(req)?
logger.log {redir: req.query.redir}, "setting explicit redirect from login page"
AuthenticationController._setRedirectInSession(req, req.query.redir)
res.render 'user/login',
title: 'login',
redir: req.query.redir,
email: req.query.email
settingsPage : (req, res, next)->
logger.log user: req.session.user, "loading settings page"
UserLocator.findById req.session.user._id, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user: user_id, "loading settings page"
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
UserLocator.findById user_id, (err, user)->
return next(err) if err?
res.render 'user/settings',
title:'account_settings'
user: user,
shouldAllowEditingDetails: shouldAllowEditingDetails
languages: Settings.languages,
accountSettingsTabActive: true
sessionsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
logger.log user_id: user._id, "loading sessions page"
UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) ->
if err?
logger.err {user_id: user._id}, "error getting all user sessions"
return next(err)
res.render 'user/sessions',
title: "sessions"
sessions: sessions

View file

@ -3,14 +3,11 @@ redis = require('redis-sharelatex')
logger = require("logger-sharelatex")
Async = require('async')
_ = require('underscore')
UserSessionsRedis = require('./UserSessionsRedis')
rclient = redis.createClient(Settings.redis.web)
rclient = UserSessionsRedis.client()
module.exports = UserSessionsManager =
_sessionSetKey: (user) ->
return "UserSessions:#{user._id}"
# mimic the key used by the express sessions
_sessionKey: (sessionId) ->
return "sess:#{sessionId}"
@ -23,7 +20,7 @@ module.exports = UserSessionsManager =
logger.log {user_id: user._id}, "no sessionId to track, returning"
return callback(null)
logger.log {user_id: user._id, sessionId}, "onLogin handler"
sessionSetKey = UserSessionsManager._sessionSetKey(user)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
value = UserSessionsManager._sessionKey sessionId
rclient.multi()
.sadd(sessionSetKey, value)
@ -43,7 +40,7 @@ module.exports = UserSessionsManager =
logger.log {user_id: user._id}, "no sessionId to untrack, returning"
return callback(null)
logger.log {user_id: user._id, sessionId}, "onLogout handler"
sessionSetKey = UserSessionsManager._sessionSetKey(user)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
value = UserSessionsManager._sessionKey sessionId
rclient.multi()
.srem(sessionSetKey, value)
@ -55,15 +52,45 @@ module.exports = UserSessionsManager =
UserSessionsManager._checkSessions(user, () ->)
callback()
getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) ->
exclude = _.map(exclude, UserSessionsManager._sessionKey)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
rclient.smembers sessionSetKey, (err, sessionKeys) ->
if err?
logger.err user_id: user._id, "error getting all session keys for user from redis"
return callback(err)
sessionKeys = _.filter sessionKeys, (k) -> !(_.contains(exclude, k))
if sessionKeys.length == 0
logger.log {user_id: user._id}, "no other sessions found, returning"
return callback(null, [])
Async.mapSeries sessionKeys, ((k, cb) -> rclient.get(k, cb)), (err, sessions) ->
if err?
logger.err {user_id: user._id}, "error getting all sessions for user from redis"
return callback(err)
result = []
for session in sessions
if session is null
continue
session = JSON.parse(session)
session_user = session?.user or session?.passport?.user
result.push {
ip_address: session_user.ip_address,
session_created: session_user.session_created
}
return callback(null, result)
revokeAllUserSessions: (user, retain, callback=(err)->) ->
if !retain
if !retain?
retain = []
retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
if !user
if !user?
logger.log {}, "no user to revoke sessions for, returning"
return callback(null)
logger.log {user_id: user._id}, "revoking all existing sessions for user"
sessionSetKey = UserSessionsManager._sessionSetKey(user)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
rclient.smembers sessionSetKey, (err, sessionKeys) ->
if err?
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
@ -73,12 +100,18 @@ module.exports = UserSessionsManager =
logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning"
return callback(null)
logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user"
rclient.multi()
.del(keysToDelete)
.srem(sessionSetKey, keysToDelete)
.exec (err, result) ->
deletions = keysToDelete.map (k) ->
(cb) ->
rclient.del k, cb
Async.series deletions, (err, _result) ->
if err?
logger.err {err, user_id: user._id, sessionSetKey}, "errror revoking all sessions for user"
return callback(err)
rclient.srem sessionSetKey, keysToDelete, (err) ->
if err?
logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user"
logger.err {err, user_id: user._id, sessionSetKey}, "error removing session set for user"
return callback(err)
callback(null)
@ -86,7 +119,7 @@ module.exports = UserSessionsManager =
if !user
logger.log {}, "no user to touch sessions for, returning"
return callback(null)
sessionSetKey = UserSessionsManager._sessionSetKey(user)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) ->
if err?
logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set"
@ -98,7 +131,7 @@ module.exports = UserSessionsManager =
logger.log {}, "no user, returning"
return callback(null)
logger.log {user_id: user._id}, "checking sessions for user"
sessionSetKey = UserSessionsManager._sessionSetKey(user)
sessionSetKey = UserSessionsRedis.sessionSetKey(user)
rclient.smembers sessionSetKey, (err, sessionKeys) ->
if err?
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"

View file

@ -0,0 +1,21 @@
Settings = require 'settings-sharelatex'
redis = require 'redis-sharelatex'
ioredis = require 'ioredis'
logger = require 'logger-sharelatex'
redisSessionsSettings = Settings.redis.websessions or Settings.redis.web
module.exports = Redis =
client: () ->
if redisSessionsSettings?.cluster?
logger.log {}, "using redis cluster for web sessions"
rclient = new ioredis.Cluster(redisSessionsSettings.cluster)
else
rclient = redis.createClient(redisSessionsSettings)
return rclient
sessionSetKey: (user) ->
if redisSessionsSettings?.cluster?
return "UserSessions:{#{user._id}}"
else
return "UserSessions:#{user._id}"

View file

@ -5,42 +5,54 @@ Settings = require('settings-sharelatex')
SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters')
querystring = require('querystring')
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
AuthenticationController = require("../Features/Authentication/AuthenticationController")
_ = require("underscore")
async = require("async")
Modules = require "./Modules"
Url = require "url"
PackageVersions = require "./PackageVersions"
fingerprints = {}
Path = require 'path'
jsPath =
if Settings.useMinifiedJs
"/minjs/"
else
"/js/"
ace = PackageVersions.lib('ace')
pdfjs = PackageVersions.lib('pdfjs')
logger.log "Generating file fingerprints..."
for path in [
"#{jsPath}libs/require.js",
"#{jsPath}ide.js",
"#{jsPath}main.js",
"#{jsPath}libs.js",
"#{jsPath}ace/ace.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
"/stylesheets/style.css"
]
filePath = Path.join __dirname, "../../../", "public#{path}"
getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
exists = fs.existsSync filePath
if exists
content = fs.readFileSync filePath
hash = crypto.createHash("md5").update(content).digest("hex")
logger.log "#{filePath}: #{hash}"
fingerprints[path] = hash
return content
else
logger.log filePath:filePath, "file does not exist for fingerprints"
return ""
logger.log "Generating file fingerprints..."
pathList = [
["#{jsPath}libs/require.js"]
["#{jsPath}ide.js"]
["#{jsPath}main.js"]
["#{jsPath}libs.js"]
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"]
["#{jsPath}libs/#{pdfjs}/pdf.js"]
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
["/stylesheets/style.css"]
]
for paths in pathList
contentList = _.map(paths, getFileContent)
content = contentList.join("")
hash = crypto.createHash("md5").update(content).digest("hex")
_.each paths, (filePath)->
logger.log "#{filePath}: #{hash}"
fingerprints[filePath] = hash
getFingerprint = (path) ->
if fingerprints[path]?
@ -59,12 +71,13 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.session = req.session
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
user_id = AuthenticationController.getLoggedInUserId(req)
if cdnBlocked and !req.session.cdnBlocked?
logger.log user_id:req?.session?.user?._id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
req.session.cdnBlocked = true
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
@ -77,16 +90,16 @@ module.exports = (app, webRouter, apiRouter)->
staticFilesBase = Settings.cdn?.web?.darkHost
else
staticFilesBase = ""
res.locals.jsPath = jsPath
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
doFingerPrint = opts.fingerprint != false
if !opts.qs?
opts.qs = {}
@ -95,14 +108,13 @@ module.exports = (app, webRouter, apiRouter)->
if opts.cdn != false
path = Url.resolve(staticFilesBase, path)
qs = querystring.stringify(opts.qs)
if qs? and qs.length > 0
path = path + "?" + qs
return path
res.locals.buildCssPath = (cssFile)->
path = Path.join("/stylesheets/", cssFile)
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
@ -115,7 +127,7 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.settings = Settings
next()
@ -123,7 +135,9 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.translate = (key, vars = {}) ->
vars.appName = Settings.appName
req.i18n.translate(key, vars)
res.locals.currentUrl = req.originalUrl
# Don't include the query string parameters, otherwise Google
# treats ?nocdn=true as the canonical version
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
next()
webRouter.use (req, res, next)->
@ -131,9 +145,10 @@ module.exports = (app, webRouter, apiRouter)->
Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2)
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next) ->
res.locals.getUserEmail = ->
email = req?.session?.user?.email or ""
user = AuthenticationController.getSessionUser(req)
email = user?.email or ""
return email
next()
@ -143,15 +158,17 @@ module.exports = (app, webRouter, apiRouter)->
return formatedPrivileges[privilegeLevel] || "Private"
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.buildReferalUrl = (referal_medium) ->
url = Settings.siteUrl
if req.session? and req.session.user? and req.session.user.referal_id?
url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
currentUser = AuthenticationController.getSessionUser(req)
if currentUser? and currentUser?.referal_id?
url+="?r=#{currentUser.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
return url
res.locals.getReferalId = ->
if req.session? and req.session.user? and req.session.user.referal_id
return req.session.user.referal_id
currentUser = AuthenticationController.getSessionUser(req)
if currentUser? and currentUser?.referal_id?
return currentUser.referal_id
res.locals.getReferalTagLine = ->
tagLines = [
"Roar!"
@ -167,7 +184,11 @@ module.exports = (app, webRouter, apiRouter)->
return ""
res.locals.getLoggedInUserId = ->
return req.session.user?._id
return AuthenticationController.getLoggedInUserId(req)
res.locals.isUserLoggedIn = ->
return AuthenticationController.isUserLoggedIn(req)
res.locals.getSessionUser = ->
return AuthenticationController.getSessionUser(req)
next()
webRouter.use (req, res, next) ->
@ -179,25 +200,26 @@ module.exports = (app, webRouter, apiRouter)->
return req.query?[field]
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.fingerprint = getFingerprint
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
webRouter.use (req, res, next)->
res.locals.externalAuthenticationSystemUsed = ->
Settings.ldap?
Settings.ldap? or Settings.saml?
next()
webRouter.use (req, res, next)->
if req.session.user?
currentUser = AuthenticationController.getSessionUser(req)
if currentUser?
res.locals.user =
email: req.session.user.email
first_name: req.session.user.first_name
last_name: req.session.user.last_name
email: currentUser.email
first_name: currentUser.first_name
last_name: currentUser.last_name
if req.session.justRegistered
res.locals.justRegistered = true
delete req.session.justRegistered
@ -222,8 +244,10 @@ module.exports = (app, webRouter, apiRouter)->
for key, value of Settings.nav
res.locals.nav[key] = _.clone(Settings.nav[key])
res.locals.templates = Settings.templateLinks
if res.locals.nav.header
console.error {}, "The `nav.header` setting is no longer supported, use `nav.header_extras` instead"
next()
webRouter.use (req, res, next) ->
SystemMessageManager.getMessages (error, messages = []) ->
res.locals.systemMessages = messages
@ -246,5 +270,3 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.moduleIncludes = Modules.moduleIncludes
res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
next()

View file

@ -1,5 +1,7 @@
module.exports =
user: (user) ->
if !user?
return null
if !user._id?
user = {_id : user}
return {
@ -10,6 +12,8 @@ module.exports =
}
project: (project) ->
if !project?
return null
if !project._id?
project = {_id: project}
return {

View file

@ -18,6 +18,10 @@ module.exports = Modules =
applyRouter: (webRouter, apiRouter) ->
for module in @modules
module.router?.apply(webRouter, apiRouter)
applyNonCsrfRouter: (webRouter, apiRouter) ->
for module in @modules
module.nonCsrfRouter?.apply(webRouter, apiRouter)
viewIncludes: {}
loadViewIncludes: (app) ->
@ -25,14 +29,14 @@ module.exports = Modules =
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
@viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade"))
@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
moduleIncludes: (view, locals) ->
partials = Modules.viewIncludes[view] or []
compiledPartials = Modules.viewIncludes[view] or []
html = ""
for partial in partials
compiler = jade.compile(partial, doctype: "html")
html += compiler(locals)
for compiledPartial in compiledPartials
d = new Date()
html += compiledPartial(locals)
return html
moduleIncludesAvailable: (view) ->
@ -58,4 +62,4 @@ module.exports = Modules =
return callback(error) if error?
return callback null, results
Modules.loadModules()
Modules.loadModules()

View file

@ -0,0 +1,15 @@
version = {
"pdfjs": "1.6.210p2"
"moment": "2.9.0"
"ace": "1.2.5"
}
module.exports = {
version: version
lib: (name) ->
if version[name]?
return "#{name}-#{version[name]}"
else
return "#{name}"
}

View file

@ -8,7 +8,9 @@ expressLocals = require('./ExpressLocals')
Router = require('../router')
metrics.inc("startup")
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
UserSessionsRedis = require('../Features/User/UserSessionsRedis')
sessionsRedisClient = UserSessionsRedis.client()
session = require("express-session")
RedisStore = require('connect-redis')(session)
@ -19,7 +21,11 @@ csrf = require('csurf')
csrfProtection = csrf()
cookieParser = require('cookie-parser')
sessionStore = new RedisStore(client:rclient)
# Init the session store
sessionStore = new RedisStore(client:sessionsRedisClient)
passport = require('passport')
LocalStrategy = require('passport-local').Strategy
Mongoose = require("./Mongoose")
@ -32,6 +38,7 @@ Modules = require "./Modules"
ErrorController = require "../Features/Errors/ErrorController"
UserSessionsManager = require "../Features/User/UserSessionsManager"
AuthenticationController = require "../Features/Authentication/AuthenticationController"
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
@ -83,6 +90,29 @@ webRouter.use session
secure: Settings.secureCookie
store: sessionStore
key: Settings.cookieName
rolling: true
# passport
webRouter.use passport.initialize()
webRouter.use passport.session()
passport.use(new LocalStrategy(
{
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password'
},
AuthenticationController.doPassportLogin
))
passport.serializeUser(AuthenticationController.serializeUser)
passport.deserializeUser(AuthenticationController.deserializeUser)
Modules.hooks.fire 'passportSetup', passport, (err) ->
if err?
logger.err {err}, "error setting up passport in modules"
Modules.applyNonCsrfRouter(webRouter, apiRouter)
webRouter.use csrfProtection
webRouter.use translations.expressMiddlewear
webRouter.use translations.setLangBasedOnDomainMiddlewear
@ -90,8 +120,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear
# Measure expiry from last request, not last login
webRouter.use (req, res, next) ->
req.session.touch()
if req?.session?.user?
UserSessionsManager.touch(req.session.user, (err)->)
if AuthenticationController.isUserLoggedIn(req)
UserSessionsManager.touch(AuthenticationController.getSessionUser(req), (err)->)
next()
webRouter.use ReferalConnect.use

View file

@ -32,6 +32,7 @@ ProjectSchema = new Schema
archived : { type: Boolean }
deletedDocs : [DeletedDocSchema]
imageName : { type: String }
track_changes : { type: Boolean }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id?

View file

@ -26,6 +26,7 @@ UserSchema = new Schema
autoComplete: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean}
}
features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
@ -38,7 +39,7 @@ UserSchema = new Schema
references: { type:Boolean, default: Settings.defaultFeatures.references }
}
featureSwitches : {
pdfng: { type: Boolean }
track_changes: { type: Boolean }
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]

View file

@ -11,7 +11,6 @@ SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
UploadsRouter = require './Features/Uploads/UploadsRouter'
metrics = require('./infrastructure/Metrics')
ReferalController = require('./Features/Referal/ReferalController')
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController")
NotificationsController = require("./Features/Notifications/NotificationsController")
@ -22,10 +21,11 @@ UserPagesController = require('./Features/User/UserPagesController')
DocumentController = require('./Features/Documents/DocumentController')
CompileManager = require("./Features/Compile/CompileManager")
CompileController = require("./Features/Compile/CompileController")
ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
HistoryController = require("./Features/History/HistoryController")
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
ChatController = require("./Features/Chat/ChatController")
@ -39,6 +39,9 @@ ReferencesController = require('./Features/References/ReferencesController')
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex")
_ = require("underscore")
@ -52,7 +55,8 @@ module.exports = class Router
webRouter.get '/login', UserPagesController.loginPage
AuthenticationController.addEndpointToLoginWhitelist '/login'
webRouter.post '/login', AuthenticationController.login
webRouter.post '/login', AuthenticationController.passportLogin
webRouter.get '/logout', UserController.logout
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
@ -70,12 +74,12 @@ module.exports = class Router
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
AnalyticsRouter.apply(webRouter, apiRouter)
Modules.applyRouter(webRouter, apiRouter)
if Settings.enableSubscriptions
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
webRouter.get '/blog', BlogController.getIndexPage
webRouter.get '/blog/*', BlogController.getPage
@ -87,8 +91,11 @@ module.exports = class Router
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
@ -166,9 +173,14 @@ module.exports = class Router
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges
webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers
webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange
webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges
webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@ -181,7 +193,10 @@ module.exports = class Router
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
webRouter.get '/announcements', AuthenticationController.requireLogin(), AnnouncementsController.getUndreadAnnouncements
# Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
@ -215,8 +230,14 @@ module.exports = class Router
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
# Note: Read only users can still comment
webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment
webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads
webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread
webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
@ -251,14 +272,27 @@ module.exports = class Router
apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
project_id = req.params.Project_id
sendRes = _.once (statusCode, message)->
res.writeHead statusCode
res.end message
CompileManager.compile req.params.Project_id, "test-compile", {}, () ->
sendRes 200, "Compiler returned in less than 10 seconds"
setTimeout (() ->
res.status statusCode
res.send message
ClsiCookieManager.clearServerId project_id # force every compile to a new server
# set a timeout
handler = setTimeout (() ->
sendRes 500, "Compiler timed out"
handler = null
), 10000
# use a valid user id for testing
test_user_id = "123456789012345678901234"
# run the compile
CompileManager.compile project_id, test_user_id, {}, (error, status) ->
clearTimeout handler if handler?
if error?
sendRes 500, "Compiler returned error #{error.message}"
else if status is "success"
sendRes 200, "Compiler returned in less than 10 seconds"
else
sendRes 500, "Compiler returned failure #{status}"
apiRouter.get "/ip", (req, res, next) ->
res.send({

View file

@ -3,7 +3,8 @@ html(itemscope, itemtype='http://schema.org/Product')
head
title Something went wrong
link(rel="icon", href="/favicon.ico")
link(rel='stylesheet', href=buildCssPath('/style.css'))
if buildCssPath
link(rel='stylesheet', href=buildCssPath('/style.css'))
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
body
.content
@ -12,7 +13,9 @@ html(itemscope, itemtype='http://schema.org/Product')
.col-md-8.col-md-offset-2.text-center
.page-header
h2 Oh dear, something went wrong.
p: img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
if buildImgPath
p
img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
p
| Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
p

View file

@ -11,4 +11,4 @@ block content
| Sorry, ShareLaTeX is briefly down for maintenance.
| We should be back within minutes, but if not, or you have
| an urgent request, please contact us at
| support@sharelatex.com
| #{settings.adminEmail}

View file

@ -51,15 +51,17 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
block scripts
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
if (cdnBlocked && !noCdnAlreadyInUrl) {
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
window.location.search += '&'+noCdnKey;
}
block scripts
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script.
@ -110,23 +112,27 @@ html(itemscope, itemtype='http://schema.org/Product')
include layout/navbar
block content
div(ng-controller="AbTestController")
- if(typeof(suppressFooter) == "undefined")
include layout/footer
- if (typeof(lookingForScribtex) != "undefined" && lookingForScribtex)
span(ng-controller="ScribtexPopupController")
include scribtex-modal
- if(typeof(suppressFooter) == "undefined")
block requirejs
script(type='text/javascript').
// minimal requirejs configuration (can be extended/overridden)
window.requirejs = {
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"paths" : {
"moment": "libs/moment-2.7.0"
"moment": "libs/#{lib('moment')}"
},
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{
"moment":{
"noGlobal": true
}
}
};
script(
@ -135,7 +141,6 @@ html(itemscope, itemtype='http://schema.org/Product')
src=buildJsPath('libs/require.js')
)
include contact-us-modal
include sentry

View file

@ -13,7 +13,7 @@ nav.navbar.navbar-default
.navbar-collapse.collapse(collapse="navCollapsed")
ul.nav.navbar-nav.navbar-right
if (session && session.user && session.user.isAdmin)
if (getSessionUser() && getSessionUser().isAdmin)
li.dropdown(class="subdued", dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| Admin
@ -24,8 +24,11 @@ nav.navbar.navbar-default
li
a(href="/admin/user") Manage Users
each item in nav.header
if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
// loop over header_extras
each item in nav.header_extras
if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
if item.dropdown
li.dropdown(class=item.class, dropdown)
a.dropdown-toggle(href, dropdown-toggle)
@ -47,7 +50,35 @@ nav.navbar.navbar-default
a(href=item.url, class=item.class) !{translate(item.text)}
else
| !{translate(item.text)}
// logged out
if !getSessionUser()
// register link
if !externalAuthenticationSystemUsed()
li
a(href="/register") #{translate('register')}
// login link
li
a(href="/login") #{translate('log_in')}
// projects link and account menu
if getSessionUser()
li
a(href="/project") #{translate('Projects')}
li.dropdown(dropdown)
a.dropbodw-toggle(href, dropdown-toggle)
| #{translate('Account')}
b.caret
ul.dropdown-menu
li
div.subdued #{getUserEmail()}
li.divider
li
a(href="/user/settings") #{translate('Account Settings')}
if nav.showSubscriptionLink
li
a(href="/user/subscription") #{translate('subscription')}
li.divider
li
a(href="/logout") #{translate('log_out')}

View file

@ -3,7 +3,6 @@ extends ../layout
block vars
- var suppressNavbar = true
- var suppressFooter = true
- var suppressDefaultJs = true
- var suppressSystemMessages = true
block content
@ -16,7 +15,8 @@ block content
p.text-center.text-danger(ng-if="state.error").ng-cloak
span(ng-bind-html="state.error")
include ./editor/feature-onboarding
.global-alerts(ng-cloak)
.alert.alert-danger.small(ng-if="connection.forced_disconnect")
strong #{translate("disconnected")}
@ -38,7 +38,7 @@ block content
include ./editor/left-menu
#chat-wrapper(
#chat-wrapper.full-size(
layout="chat",
spacing-open="12",
spacing-closed="0",
@ -66,7 +66,7 @@ block content
.ui-layout-center
include ./editor/editor
include ./editor/binary-file
include ./editor/track-changes
include ./editor/history
include ./editor/publish-template
.ui-layout-east
@ -86,7 +86,17 @@ block content
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")}
script(src='/socket.io/socket.io.js')
script(type="text/ng-template", id="lockEditorModalTemplate")
.modal-header
h3 {{ title }}
.modal-body(ng-bind-html="message")
block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js')
//- don't use cdn for workers
- var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false})
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag.
@ -97,50 +107,46 @@ block content
window.csrfToken = "!{csrfToken}";
window.anonymous = #{anonymous};
window.maxDocLength = #{maxDocLength};
window.trackChangesEnabled = #{trackChangesEnabled};
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
"moment": "libs/moment-2.7.0",
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
"shim": {
"libs/pdf": {
deps: ["libs/pdfjs-1.3.91/compatibility"]
"pdfjs-dist/build/pdf": {
"deps": ["libs/#{lib('pdfjs')}/compatibility"]
},
"ace/ext-searchbox": {
deps: ["ace/ace"]
"deps": ["ace/ace"]
},
"ace/ext-modelist": {
"deps": ["ace/ace"]
},
"ace/ext-language_tools": {
deps: ["ace/ace"]
"deps": ["ace/ace"]
}
},
config:{
moment:{
noGlobal: true
"config":{
"moment":{
"noGlobal": true
}
}
};
window.aceFingerprint = "#{fingerprint(jsPath + 'ace/ace.js')}"
- locals.suppressDefaultJs = true
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
- var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn
script(type='text/javascript').
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
window.aceWorkerPath = "#{aceWorkerPath}";
script(
data-main=buildJsPath("ide.js", {fingerprint:false}),
baseurl=fullJsPath,
data-ace-base=buildJsPath('ace', {fingerprint:false}),
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
src=buildJsPath('libs/require.js')
)

View file

@ -7,8 +7,21 @@ div.full-size(
resize-proportionally="true"
initial-size-east="'50%'"
minimum-restore-size-east="300"
allow-overflow-on="'center'"
)
.ui-layout-center
.ui-layout-center(
ng-controller="ReviewPanelController",
ng-class="{\
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\
'rp-loading-threads': reviewPanel.loadingThreads\
}"
)
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
span(ng-show="editor.open_doc_id")
i.fa.fa-spin.fa-refresh
@ -24,7 +37,7 @@ div.full-size(
keybindings="settings.mode",
font-size="settings.fontSize",
auto-complete="settings.autoComplete",
spell-check="true",
spell-check="!anonymous",
spell-check-language="project.spellCheckLanguage",
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
show-print-margin="false",
@ -32,12 +45,31 @@ div.full-size(
last-updated="editor.last_updated",
cursor-position="editor.cursorPosition",
goto-line="editor.gotoLine",
resize-on="layout:main:resize,layout:pdf:resize",
resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle",
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
read-only="!permissions.write",
on-ctrl-enter="recompileViaKey"
file-name="editor.open_doc_name",
on-ctrl-enter="recompileViaKey",
syntax-validation="settings.syntaxValidation",
review-panel="reviewPanel",
events-bridge="reviewPanelEventsBridge"
track-changes-enabled="project.features.trackChanges",
track-changes= "editor.trackChanges",
doc-id="editor.open_doc_id"
renderer-data="reviewPanel.rendererData"
)
a.rp-track-changes-indicator(
href
ng-if="editor.wantTrackChanges"
ng-click="toggleReviewPanel();"
ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
) Track changes is
strong on
include ./review-panel
.ui-layout-east
div(ng-if="ui.pdfLayout == 'sideBySide'")
include ./pdf

View file

@ -0,0 +1,54 @@
.feat-onboard(
ng-controller="FeatureOnboardingController"
ng-class="('feat-onboard-step' + innerStep)"
ng-if="!state.loading && ui.showCodeCheckerOnboarding"
ng-cloak
)
.feat-onboard-wrapper
h1.feat-onboard-title
| Introducing&nbsp;
span.feat-onboard-title-name Code check
div(ng-if="innerStep === 1;")
p.feat-onboard-description
span.feat-onboard-description-name Code check&nbsp;
| will highlight potential problems in your LaTeX code, allowing you to handle errors earlier and become more productive.
.row
video.feat-onboard-video(autoplay, loop)
source(src="/img/teasers/code-checker/code-checker.mp4", type="video/mp4")
img(src="/img/teasers/code-checker/code-checker.gif")
.row.feat-onboard-adv-wrapper
.col-xs-4
h2.feat-onboard-adv-title
| Missing&nbsp;
span.feat-onboard-adv-title-highlight brackets
p Forgot to place a closing bracket? We'll warn you.
.col-xs-4
h2.feat-onboard-adv-title
| Unclosed&nbsp;
span.feat-onboard-adv-title-highlight environments
p
| Know when you are missing an&nbsp;
code \end{...}
| &nbsp;command.
.col-xs-4
h2.feat-onboard-adv-title
| Incorrect&nbsp;
span.feat-onboard-adv-title-highlight nesting
p
| Order matters. Get notified when you use an&nbsp;
code \end{...}
| &nbsp; too soon.
.feat-onboard-btn-wrapper
button.btn.btn-primary(ng-click="turnCodeCheckOn();") Yes, turn Code check on
.feat-onboard-btn-wrapper
button.btn.btn-default(ng-click="turnCodeCheckOff();") No, disable it for now
div(ng-if="innerStep === 2;")
p.feat-onboard-description
| Remember: you can always turn&nbsp;
span.feat-onboard-description-name Code check&nbsp;
em on&nbsp;
| or&nbsp;
em off&nbsp;
|, in the settings menu.
.feat-onboard-btn-wrapper
button.btn.btn-primary(ng-click="dismiss();") OK, got it

View file

@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
)
li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'")
li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
h3 #{translate("deleted_files")}
li(
ng-class="{ 'selected': entity.selected }",
ng-repeat="entity in deletedDocs | orderBy:'name'",
ng-controller="FileTreeEntityController",
ng-show="ui.view == 'track-changes'"
ng-show="ui.view == 'history'"
)
.entity
.entity-name(
@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate')
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open"
select-name-on="open",
ng-pattern="validFileRegex",
name="name"
)
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open"
select-name-on="open",
ng-pattern="validFileRegex",
name="name"
)
.text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
)
span(ng-hide="state.inflight") #{translate("delete")}
span(ng-show="state.inflight") #{translate("deleting")}...
script(type='text/ng-template', id='invalidFileNameModalTemplate')
.modal-header
h3 #{translate('invalid_file_name')}
.modal-body
p #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-click="$close()"
) #{translate('ok')}

View file

@ -1,19 +1,19 @@
header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true"
tooltip='#{translate("menu")}',
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
i.fa.fa-fw.fa-bars
a(
href="/project"
tooltip="#{translate('back_to_projects')}",
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
i.fa.fa-fw.fa-level-up
header.toolbar.toolbar-header.toolbar-with-labels(
ng-cloak,
ng-hide="state.loading"
)
.toolbar-left
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true;",
)
i.fa.fa-fw.fa-bars
p.toolbar-label #{translate("menu")}
a(
href="/project"
)
i.fa.fa-fw.fa-level-up
span(ng-controller="PdfViewToggleController")
a(
href,
@ -85,39 +85,40 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
) {{ user.name.slice(0,1) }}
| {{ user.name }}
a.btn.btn-full-height(
href,
ng-if="project.features.trackChanges",
ng-class="{ active: ui.reviewPanelOpen }"
ng-click="toggleReviewPanel()"
)
i.review-icon
p.toolbar-label Review
a.btn.btn-full-height(
href,
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
ng-mouseenter="trackHover('share')"
ng-click="openShareProjectModal()",
ng-controller="ShareController"
ng-click="openShareProjectModal();",
ng-controller="ShareController",
)
i.fa.fa-fw.fa-group
p.toolbar-label #{translate("share")}
a.btn.btn-full-height(
href,
ng-mouseenter="trackHover('track-changes')"
ng-click="toggleTrackChanges()",
ng-class="{ active: (ui.view == 'track-changes') }"
tooltip="#{translate('recent_changes')}",
tooltip-placement="bottom"
ng-click="toggleHistory();",
ng-class="{ active: (ui.view == 'history') }",
)
i.fa.fa-fw.fa-history
p.toolbar-label #{translate("history")}
a.btn.btn-full-height(
href,
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
ng-mouseenter="trackHover('chat')"
ng-click="toggleChat()",
ng-click="toggleChat();",
ng-controller="ChatButtonController",
ng-show="!anonymous"
ng-show="!anonymous",
)
i.fa.fa-fw.fa-comment(
ng-class="{ 'bounce': unreadMessages > 0 }"
)
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
) {{ unreadMessages }}
p.toolbar-label #{translate("chat")}

View file

@ -1,61 +1,100 @@
div#trackChanges(ng-show="ui.view == 'track-changes'")
span(ng-controller="TrackChangesPremiumPopup")
.upgrade-prompt(ng-show="!project.features.versioning")
.message(ng-show="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
div#history(ng-show="ui.view == 'history'")
span(ng-controller="HistoryPremiumPopup")
.upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
div(ng-if="project.owner._id == user.id")
div(sixpack-switch="teaser-history")
.message(sixpack-default)
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
sixpack-convert="teaser-history"
) #{translate("start_free_trial")}
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('track-changes')"
) #{translate("start_free_trial")}
.message.message-wider(sixpack-when="focused")
header.message-header
h3 History
.message-body
h4.teaser-title See who changed what. Go back to previous versions.
img.teaser-img(
src="/img/teasers/history/teaser-history.png"
alt="History"
)
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
.row
.col-md-8.col-md-offset-2
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| Catch up with your collaborators changes
li
i.fa.fa-check &nbsp;
| See changes over any time period
li
i.fa.fa-check &nbsp;
| Revert your documents to previous versions
li
i.fa.fa-check &nbsp;
| Restore deleted files
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('history')"
sixpack-convert="teaser-history"
) Try it for free
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
p
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
aside.change-list(
ng-controller="TrackChangesListController"
ng-controller="HistoryListController"
infinite-scroll="loadMore()"
infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd"
infinite-scroll-initialize="ui.view == 'track-changes'"
infinite-scroll-disabled="history.loading || history.atEnd"
infinite-scroll-initialize="ui.view == 'history'"
)
.infinite-scroll-inner
ul.list-unstyled(
ng-class="{\
'hover-state': trackChanges.hoveringOverListSelectors\
'hover-state': history.hoveringOverListSelectors\
}"
)
li.change(
ng-repeat="update in trackChanges.updates"
ng-repeat="update in history.updates"
ng-class="{\
'first-in-day': update.meta.first_in_day,\
'selected': update.inSelection,\
@ -65,7 +104,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
'hover-selected-to': update.hoverSelectedTo,\
'hover-selected-from': update.hoverSelectedFrom,\
}"
ng-controller="TrackChangesListItemController"
ng-controller="HistoryListItemController"
)
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
@ -108,55 +147,55 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
.color-square(style="background-color: hsl(100, 100%, 50%)")
span #{translate("anonymous")}
.loading(ng-show="trackChanges.loading")
.loading(ng-show="history.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
.diff-panel.full-size(ng-controller="TrackChangesDiffController")
.diff-panel.full-size(ng-controller="HistoryDiffController")
.diff(
ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error"
ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error"
)
.toolbar.toolbar-alt
span.name
| <strong>{{trackChanges.diff.highlights.length}} </strong>
| <strong>{{history.diff.highlights.length}} </strong>
ng-pluralize(
count="trackChanges.diff.highlights.length",
count="history.diff.highlights.length",
when="{\
'one': 'change',\
'other': 'changes'\
}"
)
| in <strong>{{trackChanges.diff.doc.name}}</strong>
| in <strong>{{history.diff.doc.name}}</strong>
.toolbar-right
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"
) #{translate("restore_to_before_these_changes")}
.diff-editor.hide-ace-cursor(
ace-editor="track-changes",
ace-editor="history",
theme="settings.theme",
font-size="settings.fontSize",
text="trackChanges.diff.text",
highlights="trackChanges.diff.highlights",
text="history.diff.text",
highlights="history.diff.highlights",
read-only="true",
resize-on="layout:main:resize",
navigate-highlights="true"
)
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess"
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
p
a.btn.btn-primary.btn-lg(
href,
ng-click="restoreDeletedDoc()",
ng-disabled="trackChanges.diff.restoreInProgress"
ng-disabled="history.diff.restoreInProgress"
) #{translate("restore")}
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess"
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})}
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
@ -164,13 +203,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
ng-click="backToEditorAfterRestore()",
) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="trackChanges.diff.loading")
.loading-panel(ng-show="history.diff.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="trackChanges.diff.error")
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate")
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
.modal-header
button.close(
type="button"

View file

@ -66,6 +66,31 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
.hotkey
span.combination {{ctrl}} + I
span.description Italic Text
h3 #{translate("autocomplete")}
.row
.col-xs-6
.hotkey
span.combination Ctrl + Space
span.description Autocomplete Menu
.col-xs-6
.hotkey
span.combination Tab / Up / Down
span.description Select Candidate
.hotkey
span.combination Enter
span.description Insert Candidate
h3 !{translate("autocomplete_references")}
.row
.col-xs-6
.hotkey
span.combination Ctrl + Space
span.description Search References
.modal-footer
button.btn.btn-default(
ng-click="cancel()"

View file

@ -105,6 +105,14 @@ aside#left-menu.full-size(
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
)
.form-controls.code-check-setting
label(for="syntaxValidation") #{translate("syntax_validation")}
select(
name="syntaxValidation"
ng-model="settings.syntaxValidation"
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
)
.form-controls
label(for="theme") #{translate("theme")}
select(
@ -197,6 +205,10 @@ script(type='text/ng-template', id='wordCountModalTemplate')
)
div(ng-if="!status.loading")
.container-fluid
.row(ng-show='data.messages.length > 0')
.col-xs-12
.alert.alert-danger
p(style="white-space: pre-wrap") {{data.messages}}
.row
.col-xs-4
.pull-right #{translate("total_words")} :

View file

@ -229,7 +229,7 @@ div.full-size.pdf(ng-controller="PdfController")
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
p
div
.files-dropdown-container
a.btn.btn-default.btn-sm(
href,
@ -344,7 +344,7 @@ div.full-size.pdf(ng-controller="PdfController")
strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p Plus:
p
div
ul.list-unstyled
li
i.fa.fa-check &nbsp;

View file

@ -0,0 +1,332 @@
#review-panel
.review-panel-toolbar
resolved-comments-dropdown(
entries="reviewPanel.resolvedComments"
threads="reviewPanel.commentThreads"
resolved-ids="reviewPanel.resolvedThreadIds"
docs="docs"
on-open="refreshResolvedCommentsDropdown();"
on-unresolve="unresolveComment(threadId);"
on-delete="deleteComment(entryId, threadId);"
is-loading="reviewPanel.dropdown.loading"
permissions="permissions"
)
span.review-panel-toolbar-label(ng-if="permissions.write")
span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is
strong off
span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is
strong on
review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges", on-toggle="toggleTrackChanges")
span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write")
span(ng-if="editor.wantTrackChanges === false") Track Changes is
strong off
span(ng-if="editor.wantTrackChanges === true") Track Changes is
strong on
span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges")
i.fa.fa-spin.fa-spinner
.rp-entry-list(
review-panel-sorted
ng-if="reviewPanel.subView === SubViews.CUR_FILE"
)
.rp-entry-list-inner
.rp-entry-wrapper(
ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]"
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
)
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
on-reject="rejectChange(entry_id);"
on-accept="acceptChange(entry_id);"
on-indicator-click="toggleReviewPanel();"
permissions="permissions"
)
div(ng-if="entry.type === 'comment'")
comment-entry(
entry="entry"
threads="reviewPanel.commentThreads"
on-resolve="resolveComment(entry, entry_id)"
on-reply="submitReply(entry, entry_id);"
on-indicator-click="toggleReviewPanel();"
permissions="permissions"
ng-if="!reviewPanel.loadingThreads"
)
div(ng-if="entry.type === 'add-comment' && permissions.comment")
add-comment-entry(
on-start-new="startNewComment();"
on-submit="submitNewComment(content);"
on-cancel="cancelNewComment();"
on-indicator-click="toggleReviewPanel();"
)
.rp-entry-list(
ng-if="reviewPanel.subView === SubViews.OVERVIEW"
)
.rp-loading(ng-if="reviewPanel.overview.loading")
i.fa.fa-spinner.fa-spin
.rp-overview-file(
ng-repeat="doc in docs"
ng-if="!reviewPanel.overview.loading"
)
.rp-overview-file-header(
ng-if="reviewPanel.entries[doc.doc.id] | notEmpty"
)
| {{ doc.path }}
.rp-entry-wrapper(
ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries"
ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)"
)
div(ng-if="entry.type === 'insert' || entry.type === 'delete'")
change-entry(
entry="entry"
user="users[entry.metadata.user_id]"
on-indicator-click="toggleReviewPanel();"
ng-click="gotoEntry(doc.doc.id, entry)"
permissions="permissions"
)
div(ng-if="entry.type === 'comment'")
comment-entry(
entry="entry"
threads="reviewPanel.commentThreads"
on-reply="submitReply(entry, entry_id);"
on-indicator-click="toggleReviewPanel();"
ng-click="gotoEntry(doc.doc.id, entry)"
permissions="permissions"
)
.rp-nav
a.rp-nav-item(
href
ng-click="setSubView(SubViews.CUR_FILE);"
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }"
)
i.fa.fa-file-text-o
span.rp-nav-label Current file
a.rp-nav-item(
href
ng-click="setSubView(SubViews.OVERVIEW);"
ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }"
)
i.fa.fa-list
span.rp-nav-label Overview
script(type='text/ng-template', id='changeEntryTemplate')
div
.rp-entry-callout(
ng-class="'rp-entry-callout-' + entry.type"
)
.rp-entry-indicator(
ng-switch="entry.type"
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
ng-click="onIndicatorClick();"
)
i.fa.fa-pencil(ng-switch-when="insert")
i.rp-icon-delete(ng-switch-when="delete")
.rp-entry(
ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]"
)
.rp-entry-body
.rp-entry-action-icon(ng-switch="entry.type")
i.fa.fa-pencil(ng-switch-when="insert")
i.rp-icon-delete(ng-switch-when="delete")
.rp-entry-details
.rp-entry-description(ng-switch="entry.type")
span(ng-switch-when="insert") Added&nbsp;
ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
a.rp-collapse-toggle(
href
ng-if="needsCollapsing"
ng-click="toggleCollapse();"
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
span(ng-switch-when="delete") Deleted&nbsp;
del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }}
a.rp-collapse-toggle(
href
ng-if="needsCollapsing"
ng-click="toggleCollapse();"
) {{ isCollapsed ? '... (show all)' : ' (show less)' }}
.rp-entry-metadata
| {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }}&nbsp;&bull;&nbsp;
span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
.rp-entry-actions(ng-if="permissions.write")
a.rp-entry-button(href, ng-click="onReject();")
i.fa.fa-times
| &nbsp;Reject
a.rp-entry-button(href, ng-click="onAccept();")
i.fa.fa-check
| &nbsp;Accept
script(type='text/ng-template', id='commentEntryTemplate')
.rp-comment-wrapper(
ng-class="{ 'rp-comment-wrapper-resolving': state.animating }"
)
.rp-entry-callout.rp-entry-callout-comment
.rp-entry-indicator(
ng-class="{ 'rp-entry-indicator-focused': entry.focused }"
ng-click="onIndicatorClick();"
)
i.fa.fa-comment
.rp-entry.rp-entry-comment(
ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }"
)
div
.rp-comment(
ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
)
p.rp-comment-content
span.rp-entry-user(
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
) {{ comment.user.name }}:&nbsp;
| {{ comment.content }}
.rp-entry-metadata
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
.rp-loading(ng-if="!threads[entry.thread_id] || threads[entry.thread_id].submitting")
i.fa.fa-spinner.fa-spin
.rp-comment-reply(ng-if="permissions.comment")
textarea.rp-comment-input(
ng-model="entry.replyContent"
ng-keypress="handleCommentReplyKeyPress($event);"
stop-propagation="click"
placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}"
)
.rp-entry-actions
button.rp-entry-button(
ng-click="animateAndCallOnResolve();"
ng-if="permissions.comment && permissions.write"
)
i.fa.fa-inbox
| &nbsp;Resolve
button.rp-entry-button(
ng-click="onReply();"
ng-if="permissions.comment"
ng-disabled="!entry.replyContent.length"
)
i.fa.fa-reply
| &nbsp;Reply
script(type='text/ng-template', id='resolvedCommentEntryTemplate')
.rp-resolved-comment
div
.rp-resolved-comment-context
| Quoted text on&nbsp;
span.rp-resolved-comment-context-file {{ thread.docName }}
p.rp-resolved-comment-context-quote
span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}}
a.rp-collapse-toggle(
href
ng-if="needsCollapsing"
ng-click="toggleCollapse();"
) &nbsp;{{ isCollapsed ? '... (show all)' : ' (show less)' }}
.rp-comment(
ng-repeat="comment in thread.messages track by comment.id"
)
p.rp-comment-content
span.rp-entry-user(
style="color: hsl({{ comment.user.hue }}, 70%, 40%);"
ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id"
) {{ comment.user.name }}:&nbsp;
| {{ comment.content }}
.rp-entry-metadata
| {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
.rp-comment.rp-comment-resolver
p.rp-comment-resolver-content
span.rp-entry-user(
style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);"
) {{ thread.resolved_by_user.name }}:&nbsp;
| Marked as resolved.
.rp-entry-metadata
| {{ thread.resolved_at | date : 'MMM d, y h:mm a' }}
.rp-entry-actions(ng-if="permissions.comment && permissions.write")
a.rp-entry-button(
href
ng-click="onUnresolve({ 'threadId': thread.threadId });"
)
| &nbsp;Re-open
//- a.rp-entry-button(
//- href
//- ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });"
//- )
//- | &nbsp;Delete
script(type='text/ng-template', id='addCommentEntryTemplate')
div
.rp-entry-callout.rp-entry-callout-add-comment
.rp-entry-indicator(
ng-if="!commentState.adding"
ng-click="startNewComment(); onIndicatorClick();"
tooltip="Add a comment"
tooltip-placement="right"
tooltip-append-to-body="true"
)
i.fa.fa-commenting
.rp-entry.rp-entry-add-comment(
ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]"
)
a.rp-add-comment-btn(
href
ng-if="!state.isAdding"
ng-click="startNewComment();"
)
i.fa.fa-comment
| &nbsp;Add comment
div(ng-if="state.isAdding")
.rp-new-comment
textarea.rp-comment-input(
ng-model="state.content"
ng-keypress="handleCommentKeyPress($event);"
placeholder="Add your comment here"
focus-on="comment:new:open"
)
.rp-entry-actions
button.rp-entry-button(
ng-click="cancelNewComment();"
)
i.fa.fa-times
| &nbsp;Cancel
button.rp-entry-button(
ng-click="submitNewComment()"
ng-disabled="!state.content.length"
)
i.fa.fa-comment
| &nbsp;Comment
script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
.resolved-comments
.resolved-comments-backdrop(
ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }"
ng-click="state.isOpen = false"
)
a.resolved-comments-toggle(
href
ng-click="toggleOpenState();"
tooltip="Resolved Comments"
tooltip-placement="bottom"
tooltip-append-to-body="true"
)
i.fa.fa-inbox
.resolved-comments-dropdown(
ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }"
)
.rp-loading(ng-if="isLoading")
i.fa.fa-spinner.fa-spin
.resolved-comments-scroller(
ng-if="!isLoading"
)
resolved-comment-entry(
ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true"
thread="thread"
on-unresolve="handleUnresolve(threadId);"
on-delete="handleDelete(entryId, threadId);"
permissions="permissions"
)
.rp-loading(ng-if="!resolvedComments.length")
| No resolved threads.

View file

@ -137,10 +137,17 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
p.small(ng-show="startedFreeTrial")
| #{translate("refresh_page_after_starting_free_trial")}.
.modal-footer
.modal-footer.modal-footer-share
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
span.text-danger.error(ng-show="state.error")
span(ng-switch="state.errorReason")
span(ng-switch-when="cannot_invite_non_user")
| #{translate("cannot_invite_non_user")}
span(ng-switch-when="cannot_invite_self")
| #{translate("cannot_invite_self")}
span(ng-switch-default)
| #{translate("generic_something_went_wrong")}
button.btn.btn-default(
ng-click="done()"
) #{translate("close")}

View file

@ -20,7 +20,7 @@ block content
form.form(
name="acceptForm",
method="POST",
action="/project/#{invite.projectId}/invite/#{invite._id}/accept"
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
)
input(name='_csrf', type='hidden', value=csrfToken)
input(name='token', type='hidden', value="#{invite.token}")

View file

@ -19,11 +19,49 @@ block content
}
};
.content.content-alt(ng-controller="ProjectPageController")
.content.content-alt.project-list-page(ng-controller="ProjectPageController")
.container
.announcements(
ng-controller="AnnouncementsController"
ng-class="{ 'announcements-open': ui.isOpen }"
ng-cloak
)
.announcements-backdrop(
ng-if="ui.isOpen"
ng-click="toggleAnnouncementsUI();"
)
a.announcements-btn(
href
ng-if="announcements.length"
ng-click="toggleAnnouncementsUI();"
ng-class="{ 'announcements-btn-open': ui.isOpen, 'announcements-btn-has-new': ui.newItems }"
)
span.announcements-badge(ng-if="ui.newItems") {{ ui.newItems }}
.announcements-body(
ng-if="ui.isOpen"
)
.announcements-scroller
.announcement(
ng-repeat="announcement in announcements | filter:(ui.newItems ? { read: false } : '') track by announcement.id"
)
h2.announcement-header {{ announcement.title }}
p.announcement-description(ng-bind-html="announcement.excerpt")
.announcement-meta
p.announcement-date {{ announcement.date | date:"longDate" }}
a.announcement-link(
ng-href="{{ announcement.url }}"
target="_blank"
) Read more
div.text-center(
ng-if="ui.newItems > 0 && ui.newItems < announcements.length"
)
a.btn.btn-default.btn-sm(
href
ng-click="showAll();"
) Show all
.row(ng-cloak)
span(ng-show="first_sign_up == 'default' || projects.length > 0")
span(ng-if="projects.length > 0")
aside.col-md-2.col-xs-3
include ./list/side-bar
@ -31,8 +69,8 @@ block content
include ./list/notifications
include ./list/project-list
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
span(ng-if="projects.length === 0")
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/project-list-minimal
include ./list/empty-project-list
include ./list/modals

View file

@ -0,0 +1,46 @@
.row.row-spaced
.col-xs-12
.card.card-thin.project-list-card
div.welcome.text-centered(ng-cloak)
h2 #{translate("welcome_to_sl")}
p #{translate("new_to_latex_look_at")}
a(href="/templates") #{translate("templates").toLowerCase()}
| #{translate("or")}
a(href="/learn") #{translate("latex_help_guide")}
.row
.col-md-offset-4.col-md-4
.dropdown.minimal-create-proj-dropdown(dropdown)
a.btn.btn-success.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| Create First Project
ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu")
li
a(
href,
ng-click="openCreateProjectModal()"
) #{translate("blank_project")}
li
a(
href,
ng-click="openCreateProjectModal('example')"
) #{translate("example_project")}
li
a(
href,
ng-click="openUploadProjectModal()"
) #{translate("upload_project")}
!= moduleIncludes("newProjectMenu", locals)
if (templates)
li.divider
li.dropdown-header #{translate("templates")}
each item in templates
li
a.menu-indent(href=item.url) #{translate(item.name)}

View file

@ -4,14 +4,31 @@ span(ng-controller="NotificationsController").userNotifications
ng-cloak
)
li.notification_entry(
ng-repeat="unreadNotification in notifications",
ng-repeat="notification in notifications",
)
.row(ng-hide="unreadNotification.hide")
.row(ng-hide="notification.hide")
.col-xs-12
.alert.alert-info
.alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController")
div.notification_inner
span(ng-bind-html="unreadNotification.html").notification_body
.notification_body(ng-show="!notification.accepted")
| !{translate("notification_project_invite_message")}
a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
span(ng-show="!notification.inflight") #{translate("join_project")}
span(ng-show="notification.inflight")
i.fa.fa-fw.fa-spinner.fa-spin
| &nbsp;
| #{translate("joining")}...
.notification_body(ng-show="notification.accepted")
| !{translate("notification_project_invite_accepted_message")}
a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
span().notification_close
button(ng-click="dismiss(unreadNotification)").close.pull-right
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.alert.alert-info(ng-if="notification.templateKey != 'notification_project_invite'")
div.notification_inner
span(ng-bind-html="notification.html").notification_body
span().notification_close
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}

View file

@ -1,198 +0,0 @@
.row
.col-xs-12(ng-cloak)
.project-tools(ng-cloak)
.btn-toolbar(ng-show="filter != 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href='#',
tooltip="#{translate('download')}",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="downloadSelectedProjects()"
)
i.fa.fa-cloud-download
a.btn.btn-default(
href='#',
tooltip="#{translate('delete')}",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
)
i.fa.fa-trash-o
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle,
tooltip="#{translate('add_to_folders')}",
tooltip-append-to-body="true",
tooltip-placement="bottom"
)
i.fa.fa-folder-open-o
|
span.caret
ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu(
role="menu"
ng-controller="TagListController"
)
li.dropdown-header #{translate("add_to_folder")}
li(
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
ng-controller="TagDropdownItemController"
)
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
i.fa(
ng-class="{\
'fa-check-square-o': areSelectedProjectsInTag == true,\
'fa-square-o': areSelectedProjectsInTag == false,\
'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\
}"
)
| {{tag.name}}
li.divider
li
a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
.btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
a.btn.btn-default.dropdown-toggle(
href='#',
data-toggle="dropdown",
dropdown-toggle
) #{translate("more")}
span.caret
ul.dropdown-menu.dropdown-menu-right(role="menu")
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
a(
href='#',
ng-click="openRenameProjectModal()"
) #{translate("rename")}
li
a(
href='#',
ng-click="openCloneProjectModal()"
) #{translate("make_copy")}
.btn-toolbar(ng-show="filter == 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href='#',
data-original-title="Restore",
data-toggle="tooltip",
data-placement="bottom",
ng-click="restoreSelectedProjects()"
) #{translate("restore")}
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-danger(
href='#',
data-original-title="Delete Forever",
data-toggle="tooltip",
data-placement="bottom",
ng-click="openDeleteProjectsModal()"
) #{translate("delete_forever")}
.row.row-spaced
.col-xs-12
.card.card-thin.project-list-card
ul.list-unstyled.project-list.structured-list(
select-all-list,
ng-if="projects.length > 0",
max-height="projectListHeight - 25",
ng-cloak
)
li.container-fluid
.row
.col-xs-6
input.select-all(
select-all,
type="checkbox"
)
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
i.tablesort.fa(ng-class="getSortIconClass('name')")
.col-xs-2
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
.col-xs-4
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
li.project_entry.container-fluid(
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"
)
.row
.col-xs-6
input.select-item(
select-individual,
type="checkbox",
ng-model="project.selected"
)
span
a.projectName(href="/project/{{project.id}}") {{project.name}}
span(
ng-controller="TagListController"
)
a.label.label-default.tag-label(
href,
ng-repeat='tag in project.tags',
ng-click="selectTag(tag)"
) {{tag.name}}
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}
li(
ng-if="visibleProjects.length == 0",
ng-cloak
)
.row
.col-xs-12.text-centered
small #{translate("no_projects")}
div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
h2 #{translate("welcome_to_sl")}
p #{translate("new_to_latex_look_at")}
a(href="/templates") #{translate("templates").toLowerCase()}
| #{translate("or")}
a(href="/learn") #{translate("latex_help_guide")}
.row
.col-md-offset-4.col-md-4
.dropdown.minimal-create-proj-dropdown(dropdown)
a.btn.btn-success.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| Create First Project
ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu", style="text-align:center;")
li
a(
href,
ng-click="openCreateProjectModal()"
sixpack-convert="first_sign_up",
) #{translate("blank_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openCreateProjectModal('example')"
) #{translate("example_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openUploadProjectModal()"
) #{translate("upload_project")}
!= moduleIncludes("newProjectMenu", locals)
if (templates)
li.divider
li.dropdown-header #{translate("templates")}
each item in templates
li
a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)}

View file

@ -8,7 +8,7 @@
input.form-control.col-md-7.col-xs-12(
placeholder="#{translate('search_projects')}…",
autofocus='autofocus',
ng-model="searchText",
ng-model="searchText.value",
focus-on='search:clear',
ng-keyup="searchProjects()"
)
@ -16,7 +16,7 @@
i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()",
style="cursor: pointer;",
ng-show="searchText.length > 0"
ng-show="searchText.value.length > 0"
)
//- i.fa.fa-remove

View file

@ -109,52 +109,17 @@
) #{translate("complete")}
.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
span(sixpack-default).text-centered
hr
p.small #{translate("on_free_sl")}
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered
hr
p.small #{translate("on_free_sl")}
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(sixpack-when="random").text-centered
span(ng-if="randomView == 'default'")
hr
p.small #{translate("on_free_sl")}
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(ng-if="randomView == 'dropbox'")
hr
.card.card-thin
p
span Get Dropbox Sync
p
img(src=buildImgPath("dropbox/simple_logo.png"))
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(ng-if="randomView == 'github'")
hr
.card.card-thin
p
span Get Github Sync
p
img(src=buildImgPath("github/octocat.jpg"))
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
script.
window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}

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