Merge branch 'master' into node-6.9

This commit is contained in:
Shane Kilkelly 2017-02-14 11:01:14 +00:00
commit 621a07aff2
179 changed files with 5083 additions and 5069 deletions

View file

@ -1,5 +1,6 @@
fs = require "fs" fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions" PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
require('es6-promise').polyfill()
module.exports = (grunt) -> module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-coffee' grunt.loadNpmTasks 'grunt-contrib-coffee'
@ -18,6 +19,7 @@ module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-watch' grunt.loadNpmTasks 'grunt-contrib-watch'
grunt.loadNpmTasks 'grunt-parallel' grunt.loadNpmTasks 'grunt-parallel'
grunt.loadNpmTasks 'grunt-exec' grunt.loadNpmTasks 'grunt-exec'
grunt.loadNpmTasks 'grunt-postcss'
# grunt.loadNpmTasks 'grunt-contrib-imagemin' # grunt.loadNpmTasks 'grunt-contrib-imagemin'
# grunt.loadNpmTasks 'grunt-sprity' # grunt.loadNpmTasks 'grunt-sprity'
@ -136,8 +138,14 @@ module.exports = (grunt) ->
files: files:
"public/stylesheets/style.css": "public/stylesheets/style.less" "public/stylesheets/style.css": "public/stylesheets/style.less"
postcss:
options:
map: true,
processors: [
require('autoprefixer')({browsers: [ 'last 2 versions', 'ie >= 10' ]})
]
dist:
src: 'public/stylesheets/style.css'
env: env:
run: run:
@ -222,11 +230,11 @@ module.exports = (grunt) ->
sed: sed:
version: version:
path: "app/views/sentry.jade" path: "app/views/sentry.pug"
pattern: '@@COMMIT@@', pattern: '@@COMMIT@@',
replacement: '<%= commit %>', replacement: '<%= commit %>',
release: release:
path: "app/views/sentry.jade" path: "app/views/sentry.pug"
pattern: "@@RELEASE@@" pattern: "@@RELEASE@@"
replacement: process.env.BUILD_NUMBER || "(unknown build)" replacement: process.env.BUILD_NUMBER || "(unknown build)"
@ -366,7 +374,7 @@ module.exports = (grunt) ->
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server'] grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes'] grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] grunt.registerTask 'compile:css', 'Compile the less files to css', ['less', 'postcss:dist']
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",] grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
@ -389,5 +397,5 @@ module.exports = (grunt) ->
grunt.registerTask 'default', 'run' grunt.registerTask 'default', 'run'
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed'] grunt.registerTask 'version', "Write the version number into sentry.pug", ['git-rev-parse', 'sed']

View file

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

View file

@ -1,24 +1,46 @@
AnalyticsManager = require("../Analytics/AnalyticsManager") AnalyticsManager = require("../Analytics/AnalyticsManager")
BlogHandler = require("../Blog/BlogHandler") BlogHandler = require("../Blog/BlogHandler")
async = require("async")
_ = require("lodash")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
async = require("async")
_ = require("lodash")
module.exports = module.exports = AnnouncementsHandler =
_domainSpecificAnnouncements : (email)->
domainSpecific = _.filter settings?.domainAnnouncements, (domainAnnouncment)->
matches = _.filter domainAnnouncment.domains, (domain)->
return email.indexOf(domain) != -1
return matches.length > 0 and domainAnnouncment.id?
return domainSpecific or []
getUnreadAnnouncements : (user, callback = (err, announcements)->)->
if !user? and !user._id?
return callback("user not supplied")
getUnreadAnnouncements : (user_id, callback = (err, announcements)->)->
async.parallel { async.parallel {
lastEvent: (cb)-> lastEvent: (cb)->
AnalyticsManager.getLastOccurance user_id, "announcement-alert-dismissed", cb AnalyticsManager.getLastOccurance user._id, "announcement-alert-dismissed", cb
announcements: (cb)-> announcements: (cb)->
BlogHandler.getLatestAnnouncements cb BlogHandler.getLatestAnnouncements cb
}, (err, results)-> }, (err, results)->
if err? if err?
logger.err err:err, user_id:user_id, "error getting unread announcements" logger.err err:err, user_id:user._id, "error getting unread announcements"
return callback(err) return callback(err)
announcements = _.sortBy(results.announcements, "date").reverse() domainSpecific = AnnouncementsHandler._domainSpecificAnnouncements(user?.email)
domainSpecific = _.map domainSpecific, (domainAnnouncment)->
try
domainAnnouncment.date = new Date(domainAnnouncment.date)
return domainAnnouncment
catch e
return callback(e)
announcements = results.announcements
announcements = _.union announcements, domainSpecific
announcements = _.sortBy(announcements, "date").reverse()
lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId lastSeenBlogId = results?.lastEvent?.segmentation?.blogPostId
@ -35,6 +57,6 @@ module.exports =
announcement.read = read announcement.read = read
return announcement return announcement
logger.log announcementsLength:announcements?.length, user_id:user_id, "returning announcements" logger.log announcementsLength:announcements?.length, user_id:user?._id, "returning announcements"
callback null, announcements callback null, announcements

View file

@ -148,6 +148,7 @@ module.exports = AuthenticationController =
return next() return next()
else else
logger.log url:req.url, "user trying to access endpoint not in global whitelist" logger.log url:req.url, "user trying to access endpoint not in global whitelist"
AuthenticationController._setRedirectInSession(req)
return res.redirect "/login" return res.redirect "/login"
httpAuth: basicAuth (user, pass)-> httpAuth: basicAuth (user, pass)->
@ -193,8 +194,8 @@ module.exports = AuthenticationController =
_setRedirectInSession: (req, value) -> _setRedirectInSession: (req, value) ->
if !value? if !value?
value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}"
if req.session? if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$'))
req.session.postLoginRedirect = value req.session.postLoginRedirect = value
_getRedirectFromSession: (req) -> _getRedirectFromSession: (req) ->

View file

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

View file

@ -1,33 +1,34 @@
ChatHandler = require("./ChatHandler") ChatApiHandler = require("./ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController") EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController') AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
CommentsController = require('../Comments/CommentsController')
module.exports = module.exports =
sendMessage: (req, res, next)-> sendMessage: (req, res, next)->
project_id = req.params.Project_id project_id = req.params.project_id
messageContent = req.body.content content = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req) user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id? if !user_id?
err = new Error('no logged-in user') err = new Error('no logged-in user')
return next(err) return next(err)
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)-> ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) ->
if err? return next(err) if err?
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api" UserInfoManager.getPersonalInfo message.user_id, (err, user) ->
return res.sendStatus(500) return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)-> message.user = UserInfoController.formatPersonalInfo(user)
res.send() EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)->
res.send(204)
getMessages: (req, res)-> getMessages: (req, res, next)->
project_id = req.params.Project_id project_id = req.params.project_id
query = req.query query = req.query
logger.log project_id:project_id, query:query, "getting messages" logger.log project_id:project_id, query:query, "getting messages"
ChatHandler.getMessages project_id, query, (err, messages)-> ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) ->
if err? return next(err) if err?
logger.err err:err, query:query, "problem getting messages from chat api" CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) ->
return res.sendStatus 500 return next(err) if err?
logger.log length:messages?.length, "sending messages to client" logger.log length: messages?.length, "sending messages to client"
res.set 'Content-Type', 'application/json' res.json messages
res.send 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,7 +11,7 @@ module.exports = CollaboratorsEmailHandler =
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&") ].join("&")
notifyUserOfProjectInvite: (project_id, email, invite, callback)-> notifyUserOfProjectInvite: (project_id, email, invite, sendingUser, callback)->
Project Project
.findOne(_id: project_id ) .findOne(_id: project_id )
.select("name owner_ref") .select("name owner_ref")
@ -24,4 +24,5 @@ module.exports = CollaboratorsEmailHandler =
name: project.name name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite) inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref owner: project.owner_ref
sendingUser_id: sendingUser._id
EmailHandler.sendEmail "projectInvite", emailOptions, callback EmailHandler.sendEmail "projectInvite", emailOptions, callback

View file

@ -10,6 +10,7 @@ EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder") NotificationsBuilder = require("../Notifications/NotificationsBuilder")
AnalyticsManger = require("../Analytics/AnalyticsManager") AnalyticsManger = require("../Analytics/AnalyticsManager")
AuthenticationController = require("../Authentication/AuthenticationController") AuthenticationController = require("../Authentication/AuthenticationController")
rateLimiter = require("../../infrastructure/RateLimiter")
module.exports = CollaboratorsInviteController = module.exports = CollaboratorsInviteController =
@ -31,12 +32,28 @@ module.exports = CollaboratorsInviteController =
callback(null, userExists) callback(null, userExists)
else else
callback(null, true) callback(null, true)
_checkRateLimit: (user_id, callback = (error) ->) ->
LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
return callback(err) if err?
if collabLimit == -1
collabLimit = 20
collabLimit = collabLimit * 10
opts =
endpointName: "invite-to-project-by-user-id"
timeInterval: 60 * 30
subjectName: user_id
throttle: collabLimit
rateLimiter.addCount opts, callback
inviteToProject: (req, res, next) -> inviteToProject: (req, res, next) ->
projectId = req.params.Project_id projectId = req.params.Project_id
email = req.body.email email = req.body.email
sendingUser = AuthenticationController.getSessionUser(req) sendingUser = AuthenticationController.getSessionUser(req)
sendingUserId = sendingUser._id 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" logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error? return next(error) if error?
@ -48,20 +65,24 @@ module.exports = CollaboratorsInviteController =
if !email? or email == "" if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address" logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400) return res.sendStatus(400)
CollaboratorsInviteController._checkShouldInviteEmail email, (err, shouldAllowInvite)-> CollaboratorsInviteController._checkRateLimit sendingUserId, (error, underRateLimit) ->
if err? return next(error) if error?
logger.err {err, email, projectId, sendingUserId}, "error checking if we can invite this email address" if !underRateLimit
return next(err) return res.sendStatus(429)
if !shouldAllowInvite CollaboratorsInviteController._checkShouldInviteEmail email, (err, 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? 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) return next(err)
logger.log {projectId, email, sendingUserId}, "invite created" if !shouldAllowInvite
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) logger.log {email, projectId, sendingUserId}, "not allowed to send an invite to this email address"
return res.json {invite: invite} 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) -> revokeInvite: (req, res, next) ->
projectId = req.params.Project_id projectId = req.params.Project_id

View file

@ -53,7 +53,7 @@ module.exports = CollaboratorsInviteHandler =
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) -> _sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite" 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? return callback(err) if err?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)-> CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err? return callback(err) if err?
@ -80,7 +80,7 @@ module.exports = CollaboratorsInviteHandler =
# Send email and notification in background # Send email and notification in background
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err? if err?
logger.err {projectId, email}, "error sending messages for invite" logger.err {err, projectId, email}, "error sending messages for invite"
callback(null, invite) callback(null, invite)

View file

@ -22,9 +22,15 @@ module.exports =
webRouter.post( webRouter.post(
'/project/:Project_id/invite', '/project/:Project_id/invite',
RateLimiterMiddlewear.rateLimit({ RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project" endpointName: "invite-to-project-by-project-id"
params: ["Project_id"] params: ["Project_id"]
maxRequests: 200 maxRequests: 100
timeInterval: 60 * 10
}),
RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project-by-ip"
ipOnly:true
maxRequests: 100
timeInterval: 60 * 10 timeInterval: 60 * 10
}), }),
AuthenticationController.requireLogin(), AuthenticationController.requireLogin(),

View file

@ -0,0 +1,111 @@
ChatApiHandler = require("../Chat/ChatApiHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
UserInfoManager = require('../User/UserInfoManager')
UserInfoController = require('../User/UserInfoController')
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
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
deleteThread: (req, res, next) ->
{project_id, doc_id, thread_id} = req.params
logger.log {project_id, doc_id, thread_id}, "deleting comment thread"
DocumentUpdaterHandler.deleteThread project_id, doc_id, thread_id, (err) ->
return next(err) if err?
ChatApiHandler.deleteThread project_id, thread_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "delete-thread", thread_id, (err)->
res.send 204
editMessage: (req, res, next) ->
{project_id, thread_id, message_id} = req.params
{content} = req.body
logger.log {project_id, thread_id, message_id}, "editing message thread"
ChatApiHandler.editMessage project_id, thread_id, message_id, content, (err) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "edit-message", thread_id, message_id, content, (err)->
res.send 204
deleteMessage: (req, res, next) ->
{project_id, thread_id, message_id} = req.params
logger.log {project_id, thread_id, message_id}, "deleting message"
ChatApiHandler.deleteMessage project_id, thread_id, message_id, (err, threads) ->
return next(err) if err?
EditorRealTimeController.emitToRoom project_id, "delete-message", thread_id, message_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

@ -29,6 +29,21 @@ module.exports = DocstoreManager =
error = new Error("docstore api responded with non-success code: #{res.statusCode}") 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" logger.error err: error, project_id: project_id, "error getting all docs from docstore"
callback(error) callback(error)
getAllRanges: (project_id, callback = (error) ->) ->
logger.log { project_id }, "getting all doc ranges for project in docstore api"
url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges"
request.get {
url: url
json: true
}, (error, res, docs) ->
return callback(error) if error?
if 200 <= res.statusCode < 300
callback(null, docs)
else
error = new Error("docstore api responded with non-success code: #{res.statusCode}")
logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore"
callback(error)
getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) -> getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) ->
if typeof(options) == "function" if typeof(options) == "function"
@ -45,13 +60,13 @@ module.exports = DocstoreManager =
return callback(error) if error? return callback(error) if error?
if 200 <= res.statusCode < 300 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" logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api"
callback(null, doc.lines, doc.rev, doc.version) callback(null, doc.lines, doc.rev, doc.version, doc.ranges)
else else
error = new Error("docstore api responded with non-success code: #{res.statusCode}") 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" logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore"
callback(error) callback(error)
updateDoc: (project_id, doc_id, lines, version, 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" 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}" url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}"
request.post { request.post {
@ -59,6 +74,7 @@ module.exports = DocstoreManager =
json: json:
lines: lines lines: lines
version: version version: version
ranges: ranges
}, (error, res, result) -> }, (error, res, result) ->
return callback(error) if error? return callback(error) if error?
if 200 <= res.statusCode < 300 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}" logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}"
return callback(error) 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") timer = new metrics.Timer("get-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}" 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" 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) body = JSON.parse(body)
catch error catch error
return callback(error) return callback(error)
callback null, body.lines, body.version, body.ops callback null, body.lines, body.version, body.ranges, body.ops
else else
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" 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}") callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
@ -137,15 +137,37 @@ 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}" 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}") callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
getNumberOfDocsInMemory : (callback)-> acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)-> timer = new metrics.Timer("accept-change")
try url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
body = JSON.parse body logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
catch err request.post url, (error, res, body)->
logger.err err:err, "error parsing response from doc updater about the total number of docs" timer.done()
callback(err, body?.total) 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}")
deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
timer = new metrics.Timer("delete-thread")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/comment/#{thread_id}"
logger.log {project_id, doc_id, thread_id}, "deleting comment range in document updater"
request.del url, (error, res, body)->
timer.done()
if error?
logger.error {err:error, project_id, doc_id, thread_id}, "error deleting comment range in doc updater"
return callback(error)
if res.statusCode >= 200 and res.statusCode < 300
logger.log {project_id, doc_id, thread_id}, "deleted comment rangee in document updater"
return callback(null)
else
logger.error {project_id, doc_id, thread_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" PENDINGUPDATESKEY = "PendingUpdates"
DOCLINESKEY = "doclines" DOCLINESKEY = "doclines"

View file

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

View file

@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler')
ProjectDeleter = require("../Project/ProjectDeleter") ProjectDeleter = require("../Project/ProjectDeleter")
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
EditorRealTimeController = require("./EditorRealTimeController") EditorRealTimeController = require("./EditorRealTimeController")
TrackChangesManager = require("../TrackChanges/TrackChangesManager")
async = require('async') async = require('async')
LockManager = require("../../infrastructure/LockManager") LockManager = require("../../infrastructure/LockManager")
_ = require('underscore') _ = 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,6 +1,12 @@
_ = require('underscore') _ = require('underscore')
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout") PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout") NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
BaseWithHeaderEmailLayout = require("./Layouts/BaseWithHeaderEmailLayout")
SingleCTAEmailBody = require("./Bodies/SingleCTAEmailBody")
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
@ -61,7 +67,7 @@ ShareLaTeX Co-founder
templates.passwordResetRequested = templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}" subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout layout: BaseWithHeaderEmailLayout
type:"notification" type:"notification"
plainTextTemplate: _.template """ plainTextTemplate: _.template """
Password Reset Password Reset
@ -78,36 +84,21 @@ Thank you
#{settings.appName} - <%= siteUrl %> #{settings.appName} - <%= siteUrl %>
""" """
compiledTemplate: _.template """ compiledTemplate: (opts) ->
<h2>Password Reset</h2> SingleCTAEmailBody({
<p> title: "Password Reset"
We got a request to reset your #{settings.appName} password. greeting: "Hi,"
<p> message: "We got a request to reset your #{settings.appName} password."
<center> secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;"> ctaText: "Reset password"
<div style="padding-right:10px;padding-left:10px"> ctaURL: opts.setNewPasswordUrl
<a href="<%= setNewPasswordUrl %>" style="text-decoration:none" target="_blank"> gmailGoToAction: null
<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>
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>
"""
templates.projectInvite = templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>" subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: NotificationEmailLayout layout: BaseWithHeaderEmailLayout
type:"notification" type:"notification"
plainTextTemplate: _.template """ plainTextTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
@ -118,23 +109,23 @@ Thank you
#{settings.appName} - <%= siteUrl %> #{settings.appName} - <%= siteUrl %>
""" """
compiledTemplate: _.template """ compiledTemplate: (opts) ->
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p> SingleCTAEmailBody({
<center> title: "#{ opts.project.name } &ndash; shared by #{ opts.owner.email }"
<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"> greeting: "Hi,"
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center"> message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name }&rdquo; with you."
View Project secondaryMessage: null
</span> ctaText: "View project"
</a> ctaURL: opts.inviteUrl
</center> gmailGoToAction:
<p> Thank you</p> target: opts.inviteUrl
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p> name: "View project"
""" description: "Join #{ opts.project.name } at ShareLaTeX"
})
templates.completeJoinGroupAccount = templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group" subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout layout: BaseWithHeaderEmailLayout
type:"notification" type:"notification"
plainTextTemplate: _.template """ plainTextTemplate: _.template """
Hi, please verify your email to join the <%= group_name %> and get your free premium account Hi, please verify your email to join the <%= group_name %> and get your free premium account
@ -145,22 +136,39 @@ Thank You
#{settings.appName} - <%= siteUrl %> #{settings.appName} - <%= siteUrl %>
""" """
compiledTemplate: _.template """ compiledTemplate: (opts) ->
<p>Hi, please verify your email to join the <%= group_name %> and get your free premium account</p> SingleCTAEmailBody({
<center> title: "Verify Email to join #{ opts.group_name } group"
<div style="width:200px;background-color:#a93629;border:1px solid #e24b3b;border-radius:3px;padding:15px; margin:24px;"> greeting: "Hi,"
<div style="padding-right:10px;padding-left:10px"> message: "please verify your email to join the #{ opts.group_name } group and get your free premium account."
<a href="<%= completeJoinUrl %>" style="text-decoration:none" target="_blank"> secondaryMessage: null
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center"> ctaText: "Verify now"
Verify now ctaURL: opts.completeJoinUrl
</span> gmailGoToAction: null
</a> })
</div>
</div>
</center> templates.testEmail =
<p> Thank you</p> subject: _.template "A Test Email from ShareLaTeX"
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p> 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 = module.exports =

View file

@ -4,7 +4,7 @@ Settings = require('settings-sharelatex')
nodemailer = require("nodemailer") nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport') sesTransport = require('nodemailer-ses-transport')
sgTransport = require('nodemailer-sendgrid-transport') sgTransport = require('nodemailer-sendgrid-transport')
rateLimiter = require('../../infrastructure/RateLimiter')
_ = require("underscore") _ = require("underscore")
if Settings.email? and Settings.email.fromAddress? if Settings.email? and Settings.email.fromAddress?
@ -39,24 +39,39 @@ if nm_client?
else else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent." 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 = module.exports =
sendEmail : (options, callback = (error) ->)-> sendEmail : (options, callback = (error) ->)->
logger.log receiver:options.to, subject:options.subject, "sending email" logger.log receiver:options.to, subject:options.subject, "sending email"
metrics.inc "email" checkCanSendEmail options, (err, canContinue)->
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? if err?
logger.err err:err, "error sending message" return callback(err)
else if !canContinue
logger.log "Message sent to #{options.to}" logger.log sendingUser_id:options.sendingUser_id, to:options.to, subject:options.subject, canContinue:canContinue, "rate limit hit for sending email, not sending"
callback(err) 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

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

View file

@ -53,7 +53,11 @@ module.exports =
if req.body.login_after if req.body.login_after
UserGetter.getUser user_id, {email: 1}, (err, user) -> UserGetter.getUser user_id, {email: 1}, (err, user) ->
return next(err) if err? return next(err) if err?
AuthenticationController.doLogin {email:user.email, password: password}, req, res, next AuthenticationController.afterLoginSessionSetup req, user, (err) ->
if err?
logger.err {err, email: user.email}, "Error setting up session after setting password"
return next(err)
res.json {redir: AuthenticationController._getRedirectFromSession(req) || "/project"}
else else
res.sendStatus 200 res.sendStatus 200
else else

View file

@ -197,11 +197,11 @@ module.exports = ProjectController =
user_id = null user_id = null
project_id = req.params.Project_id 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 { async.parallel {
project: (cb)-> 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)-> user: (cb)->
if !user_id? if !user_id?
cb null, defaultSettingsForAnonymousUser(user_id) cb null, defaultSettingsForAnonymousUser(user_id)
@ -267,6 +267,7 @@ module.exports = ProjectController =
pdfViewer : user.ace.pdfViewer pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation syntaxValidation: user.ace.syntaxValidation
} }
trackChangesEnabled: !!project.track_changes
privilegeLevel: privilegeLevel privilegeLevel: privilegeLevel
chatUrl: Settings.apis.chat.url chatUrl: Settings.apis.chat.url
anonymous: anonymous anonymous: anonymous

View file

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

View file

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

View file

@ -1,23 +1,21 @@
Settings = require('settings-sharelatex') RateLimiter = require('../../infrastructure/RateLimiter')
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
buildKey = (k)->
return "LoginRateLimit:#{k}"
ONE_MIN = 60 ONE_MIN = 60
ATTEMPT_LIMIT = 10 ATTEMPT_LIMIT = 10
module.exports = module.exports =
processLoginRequest: (email, callback)->
multi = rclient.multi() processLoginRequest: (email, callback) ->
multi.incr(buildKey(email)) opts =
multi.get(buildKey(email)) endpointName: 'login'
multi.expire(buildKey(email), ONE_MIN * 2) throttle: ATTEMPT_LIMIT
multi.exec (err, results)-> timeInterval: ONE_MIN * 2
loginCount = results[1] subjectName: email
allow = loginCount <= ATTEMPT_LIMIT RateLimiter.addCount opts, (err, shouldAllow) ->
callback err, allow callback(err, shouldAllow)
recordSuccessfulLogin: (email, callback = ->)-> recordSuccessfulLogin: (email, callback = ->)->
rclient.del buildKey(email), callback RateLimiter.clearRateLimit 'login', email, callback

View file

@ -19,12 +19,15 @@ module.exports = RateLimiterMiddlewear =
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
params = (opts.params or []).map (p) -> req.params[p] params = (opts.params or []).map (p) -> req.params[p]
params.push user_id params.push user_id
subjectName = params.join(":")
if opts.ipOnly
subjectName = req.ip
if !opts.endpointName? if !opts.endpointName?
throw new Error("no endpointName provided") throw new Error("no endpointName provided")
options = { options = {
endpointName: opts.endpointName endpointName: opts.endpointName
timeInterval: opts.timeInterval or 60 timeInterval: opts.timeInterval or 60
subjectName: params.join(":") subjectName: subjectName
throttle: opts.maxRequests or 6 throttle: opts.maxRequests or 6
} }
RateLimiter.addCount options, (error, canContinue)-> RateLimiter.addCount options, (error, canContinue)->

View file

@ -6,8 +6,6 @@ Project = require('../../models/Project').Project
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
util = require('util') util = require('util')
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
RecurlyWrapper = require('../Subscription/RecurlyWrapper') RecurlyWrapper = require('../Subscription/RecurlyWrapper')
SubscriptionHandler = require('../Subscription/SubscriptionHandler') SubscriptionHandler = require('../Subscription/SubscriptionHandler')
projectEntityHandler = require('../Project/ProjectEntityHandler') projectEntityHandler = require('../Project/ProjectEntityHandler')

View file

@ -7,7 +7,7 @@ fs = require "fs"
ErrorController = require "../Errors/ErrorController" ErrorController = require "../Errors/ErrorController"
AuthenticationController = require('../Authentication/AuthenticationController') AuthenticationController = require('../Authentication/AuthenticationController')
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade") homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.pug")
module.exports = HomeController = module.exports = HomeController =
index : (req,res)-> index : (req,res)->
@ -28,10 +28,10 @@ module.exports = HomeController =
externalPage: (page, title) -> externalPage: (page, title) ->
return (req, res, next = (error) ->) -> return (req, res, next = (error) ->) ->
path = Path.resolve(__dirname + "/../../../views/external/#{page}.jade") path = Path.resolve(__dirname + "/../../../views/external/#{page}.pug")
fs.exists path, (exists) -> # No error in this callback - old method in Node.js! fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
if exists if exists
res.render "external/#{page}.jade", res.render "external/#{page}.pug",
title: title title: title
else else
ErrorController.notFound(req, res, next) ErrorController.notFound(req, res, next)

View file

@ -1,20 +1,25 @@
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
Project = require("../../models/Project").Project Project = require("../../models/Project").Project
User = require("../../models/User").User UserGetter = require("../User/UserGetter")
SubscriptionLocator = require("./SubscriptionLocator") SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex") Settings = require("settings-sharelatex")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler") CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
module.exports = module.exports =
allowedNumberOfCollaboratorsInProject: (project_id, callback) -> allowedNumberOfCollaboratorsInProject: (project_id, callback) ->
getOwnerOfProject project_id, (error, owner)-> Project.findById project_id, 'owner_ref', (error, project) =>
return callback(error) if error? return callback(error) if error?
if owner.features? and owner.features.collaborators? @allowedNumberOfCollaboratorsForUser project.owner_ref, callback
callback null, owner.features.collaborators
allowedNumberOfCollaboratorsForUser: (user_id, callback) ->
UserGetter.getUser user_id, {features: 1}, (error, user) ->
return callback(error) if error?
if user.features? and user.features.collaborators?
callback null, user.features.collaborators
else else
callback null, Settings.defaultPlanCode.collaborators callback null, Settings.defaultPlanCode.collaborators
canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) -> canAddXCollaborators: (project_id, x_collaborators, callback = (error, allowed)->) ->
@allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) => @allowedNumberOfCollaboratorsInProject project_id, (error, allowed_number) =>
@ -63,8 +68,4 @@ module.exports =
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached" logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
callback(err, limitReached, subscription) callback(err, limitReached, subscription)
getOwnerOfProject = (project_id, callback)-> getOwnerIdOfProject = (project_id, callback)->
Project.findById project_id, 'owner_ref', (error, project) ->
return callback(error) if error?
User.findById project.owner_ref, (error, owner) ->
callback(error, owner)

View file

@ -4,7 +4,7 @@ editorController = require('../Editor/EditorController')
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
FileTypeManager = require('../Uploads/FileTypeManager') FileTypeManager = require('../Uploads/FileTypeManager')
uuid = require('node-uuid') uuid = require('uuid')
fs = require('fs') fs = require('fs')
module.exports = module.exports =

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,20 +1,42 @@
RangesManager = require "./RangesManager"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
request = require "request" UserInfoController = require "../User/UserInfoController"
settings = require "settings-sharelatex" DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
AuthenticationController = require "../Authentication/AuthenticationController" EditorRealTimeController = require("../Editor/EditorRealTimeController")
TrackChangesManager = require "./TrackChangesManager"
module.exports = TrackChangesController = module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) -> getAllRanges: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId req project_id = req.params.project_id
url = settings.apis.trackchanges.url + req.url logger.log {project_id}, "request for project ranges"
logger.log url: url, "proxying to track-changes api" RangesManager.getAllRanges project_id, (error, docs = []) ->
getReq = request( return next(error) if error?
url: url docs = ({id: d._id, ranges: d.ranges} for d in docs)
method: req.method res.json docs
headers:
"X-User-Id": user_id getAllChangesUsers: (req, res, next) ->
) project_id = req.params.project_id
getReq.pipe(res) logger.log {project_id}, "request for project range users"
getReq.on "error", (error) -> RangesManager.getAllChangesUsers project_id, (error, users) ->
logger.error err: error, "track-changes API error" return next(error) if error?
next(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" Project = require("../../models/Project").Project
request = require "request"
logger = require "logger-sharelatex"
module.exports = TrackChangesManager = module.exports = TrackChangesManager =
flushProject: (project_id, callback = (error) ->) -> toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) ->
logger.log project_id: project_id, "flushing project in track-changes api" Project.update {_id: project_id}, {track_changes: track_changes_on}, callback
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

@ -26,17 +26,14 @@ module.exports = UserController =
UserController.sendFormattedPersonalInfo(user, res, next) UserController.sendFormattedPersonalInfo(user, res, next)
sendFormattedPersonalInfo: (user, res, next = (error) ->) -> sendFormattedPersonalInfo: (user, res, next = (error) ->) ->
UserController._formatPersonalInfo user, (error, info) -> info = UserController.formatPersonalInfo(user)
return next(error) if error? res.send JSON.stringify(info)
res.send JSON.stringify(info)
_formatPersonalInfo: (user, callback = (error, info) ->) -> formatPersonalInfo: (user, callback = (error, info) ->) ->
callback null, { if !user?
id: user._id.toString() return {}
first_name: user.first_name formatted_user = { id: user._id.toString() }
last_name: user.last_name for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"]
email: user.email if user[key]?
signUpDate: user.signUpDate formatted_user[key] = user[key]
role: user.role return formatted_user
institution: user.institution
}

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,5 +1,4 @@
Settings = require('settings-sharelatex') Settings = require('settings-sharelatex')
redis = require('redis-sharelatex')
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
Async = require('async') Async = require('async')
_ = require('underscore') _ = require('underscore')

View file

@ -189,6 +189,7 @@ module.exports = (app, webRouter, apiRouter)->
return AuthenticationController.isUserLoggedIn(req) return AuthenticationController.isUserLoggedIn(req)
res.locals.getSessionUser = -> res.locals.getSessionUser = ->
return AuthenticationController.getSessionUser(req) return AuthenticationController.getSessionUser(req)
next() next()
webRouter.use (req, res, next) -> webRouter.use (req, res, next) ->
@ -244,6 +245,8 @@ module.exports = (app, webRouter, apiRouter)->
for key, value of Settings.nav for key, value of Settings.nav
res.locals.nav[key] = _.clone(Settings.nav[key]) res.locals.nav[key] = _.clone(Settings.nav[key])
res.locals.templates = Settings.templateLinks 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() next()
webRouter.use (req, res, next) -> webRouter.use (req, res, next) ->

View file

@ -1,6 +1,6 @@
fs = require "fs" fs = require "fs"
Path = require "path" Path = require "path"
jade = require "jade" pug = require "pug"
async = require "async" async = require "async"
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules") MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
@ -29,7 +29,7 @@ module.exports = Modules =
for module in @modules for module in @modules
for view, partial of module.viewIncludes or {} for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= [] @viewIncludes[view] ||= []
@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html") @viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
moduleIncludes: (view, locals) -> moduleIncludes: (view, locals) ->
compiledPartials = Modules.viewIncludes[view] or [] compiledPartials = Modules.viewIncludes[view] or []

View file

@ -1,15 +1,27 @@
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
redis = require("redis-sharelatex") RedisWrapper = require('./RedisWrapper')
rclient = redis.createClient(settings.redis.web) rclient = RedisWrapper.client('ratelimiter')
redback = require("redback").use(rclient) RollingRateLimiter = require('rolling-rate-limiter')
module.exports =
addCount: (opts, callback = (opts, shouldProcess)->)-> module.exports = RateLimiter =
ratelimit = redback.createRateLimit(opts.endpointName)
ratelimit.addCount opts.subjectName, opts.timeInterval, (err, callCount)-> addCount: (opts, callback = (err, shouldProcess)->)->
shouldProcess = callCount < opts.throttle namespace = "RateLimit:#{opts.endpointName}:"
callback(err, shouldProcess) k = "{#{opts.subjectName}}"
limiter = RollingRateLimiter({
redis: rclient,
namespace: namespace,
interval: opts.timeInterval * 1000,
maxInInterval: opts.throttle
})
limiter k, (err, timeLeft, actionsLeft) ->
if err?
return callback(err)
allowed = timeLeft == 0
callback(null, allowed)
clearRateLimit: (endpointName, subject, callback) -> clearRateLimit: (endpointName, subject, callback) ->
rclient.del "#{endpointName}:#{subject}", callback # same as the key which will be built by RollingRateLimiter (namespace+k)
keyName = "RateLimit:#{endpointName}:{#{subject}}"
rclient.del keyName, callback

View file

@ -0,0 +1,28 @@
Settings = require 'settings-sharelatex'
redis = require 'redis-sharelatex'
ioredis = require 'ioredis'
logger = require 'logger-sharelatex'
# A per-feature interface to Redis,
# looks up the feature in `settings.redis`
# and returns an appropriate client.
# Necessary because we don't want to migrate web over
# to redis-cluster all at once.
# TODO: consider merging into `redis-sharelatex`
module.exports = Redis =
# feature = 'websessions' | 'ratelimiter' | ...
client: (feature) ->
redisFeatureSettings = Settings.redis[feature] or Settings.redis.web
if redisFeatureSettings?.cluster?
logger.log {feature}, "creating redis-cluster client"
rclient = new ioredis.Cluster(redisFeatureSettings.cluster)
rclient.__is_redis_cluster = true
else
logger.log {feature}, "creating redis client"
rclient = redis.createClient(redisFeatureSettings)
return rclient

View file

@ -7,7 +7,6 @@ crawlerLogger = require('./CrawlerLogger')
expressLocals = require('./ExpressLocals') expressLocals = require('./ExpressLocals')
Router = require('../router') Router = require('../router')
metrics.inc("startup") metrics.inc("startup")
redis = require("redis-sharelatex")
UserSessionsRedis = require('../Features/User/UserSessionsRedis') UserSessionsRedis = require('../Features/User/UserSessionsRedis')
sessionsRedisClient = UserSessionsRedis.client() sessionsRedisClient = UserSessionsRedis.client()
@ -62,7 +61,7 @@ if Settings.behindProxy
webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) webRouter.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
app.set 'views', __dirname + '/../../views' app.set 'views', __dirname + '/../../views'
app.set 'view engine', 'jade' app.set 'view engine', 'pug'
Modules.loadViewIncludes app Modules.loadViewIncludes app

View file

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

View file

@ -2,7 +2,7 @@ Project = require('./Project').Project
Settings = require 'settings-sharelatex' Settings = require 'settings-sharelatex'
_ = require('underscore') _ = require('underscore')
mongoose = require('mongoose') mongoose = require('mongoose')
uuid = require('node-uuid') uuid = require('uuid')
Schema = mongoose.Schema Schema = mongoose.Schema
ObjectId = Schema.ObjectId ObjectId = Schema.ObjectId
@ -37,9 +37,10 @@ UserSchema = new Schema
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup } compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates } templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references } references: { type:Boolean, default: Settings.defaultFeatures.references }
trackChanges: { type:Boolean, default: Settings.defaultFeatures.trackChanges }
} }
featureSwitches : { featureSwitches : {
pdfng: { type: Boolean } track_changes: { type: Boolean }
} }
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ] refered_users: [ type:ObjectId, ref:'User' ]

View file

@ -25,7 +25,7 @@ ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController") HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController") FileStoreController = require("./Features/FileStore/FileStoreController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController") HistoryController = require("./Features/History/HistoryController")
PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter") PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter") StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
ChatController = require("./Features/Chat/ChatController") ChatController = require("./Features/Chat/ChatController")
@ -40,6 +40,8 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew
BetaProgramController = require('./Features/BetaProgram/BetaProgramController') BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
AnnouncementsController = require("./Features/Announcements/AnnouncementsController") AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
TrackChangesController = require("./Features/TrackChanges/TrackChangesController")
CommentsController = require "./Features/Comments/CommentsController"
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
_ = require("underscore") _ = require("underscore")
@ -171,9 +173,14 @@ module.exports = class Router
webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject 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/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi 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, TrackChangesController.proxyToTrackChangesApi 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/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
@ -223,8 +230,17 @@ module.exports = class Router
webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage 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.delete "/project/:project_id/doc/:doc_id/thread/:thread_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteThread
webRouter.post "/project/:project_id/thread/:thread_id/messages/:message_id/edit", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.editMessage
webRouter.delete "/project/:project_id/thread/:thread_id/messages/:message_id", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.deleteMessage
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll

View file

@ -28,10 +28,10 @@ block content
tab(heading="Open Sockets") tab(heading="Open Sockets")
.row-spaced .row-spaced
ul ul
-each agents, url in openSockets each agents, url in openSockets
li #{url} - total : #{agents.length} li #{url} - total : #{agents.length}
ul ul
-each agent in agents each agent in agents
li #{agent} li #{agent}
tab(heading="Close Editor") tab(heading="Close Editor")

View file

@ -24,10 +24,10 @@ script(type='text/ng-template', id='supportModalTemplate')
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
span(ng-bind-html="suggestion.name") span(ng-bind-html="suggestion.name")
i.fa.fa-angle-right i.fa.fa-angle-right
label.desc(ng-show="'#{getUserEmail()}'.length < 1") label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
| #{translate("email")} | #{translate("email")}
.form-group(ng-show="'#{getUserEmail()}'.length < 1") .form-group(ng-show="'"+getUserEmail()+"'.length < 1")
input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
label#title12.desc label#title12.desc
| #{translate("project_url")} (#{translate("optional")}) | #{translate("project_url")} (#{translate("optional")})
.form-group .form-group
@ -37,6 +37,6 @@ script(type='text/ng-template', id='supportModalTemplate')
.form-group .form-group
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='') textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
.form-group.text-center .form-group.text-center
input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value='#{translate("contact_us")}') input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
span(ng-show="sent") span(ng-show="sent")
p #{translate("request_sent_thank_you")} p #{translate("request_sent_thank_you")}

View file

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

View file

@ -21,6 +21,8 @@ html(itemscope, itemtype='http://schema.org/Product')
link(rel="icon", href="/favicon.ico") link(rel="icon", href="/favicon.ico")
link(rel='stylesheet', href=buildCssPath('/style.css')) link(rel='stylesheet', href=buildCssPath('/style.css'))
block _headLinks
if settings.i18n.subdomainLang if settings.i18n.subdomainLang
each subdomainDetails in settings.i18n.subdomainLang each subdomainDetails in settings.i18n.subdomainLang
if !subdomainDetails.hide if !subdomainDetails.hide
@ -30,7 +32,7 @@ html(itemscope, itemtype='http://schema.org/Product')
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor") meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
-if (typeof(meta) == "undefined") -if (typeof(meta) == "undefined")
meta(itemprop="description", name="description", content='#{translate("site_description")}') meta(itemprop="description", name="description", content=translate("site_description"))
-else -else
meta(itemprop="description", name="description" , content=meta) meta(itemprop="description", name="description" , content=meta)

View file

@ -13,9 +13,9 @@ footer.site-footer
data-toggle="dropdown", data-toggle="dropdown",
aria-haspopup="true", aria-haspopup="true",
aria-expanded="false", aria-expanded="false",
tooltip="#{translate('language')}" tooltip=translate('language')
) )
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}") figure(class="sprite-icon sprite-icon-lang sprite-icon-"+currentLngCode)
ul.dropdown-menu(role="menu") ul.dropdown-menu(role="menu")
li.dropdown-header #{translate("language")} li.dropdown-header #{translate("language")}
@ -23,7 +23,7 @@ footer.site-footer
if !subdomainDetails.hide if !subdomainDetails.hide
li.lngOption li.lngOption
a.menu-indent(href=subdomainDetails.url+currentUrl) a.menu-indent(href=subdomainDetails.url+currentUrl)
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}") figure(class="sprite-icon sprite-icon-lang sprite-icon-"+subdomainDetails.lngCode)
| #{translate(subdomainDetails.lngCode)} | #{translate(subdomainDetails.lngCode)}
//- img(src="/img/flags/24/.png") //- img(src="/img/flags/24/.png")
each item in nav.left_footer each item in nav.left_footer

View file

@ -4,7 +4,7 @@ nav.navbar.navbar-default
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}") button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
i.fa.fa-bars i.fa.fa-bars
if settings.nav.custom_logo if settings.nav.custom_logo
a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand a(href='/', style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
else if (nav.title) else if (nav.title)
a(href='/').navbar-title #{nav.title} a(href='/').navbar-title #{nav.title}
else else
@ -24,7 +24,10 @@ nav.navbar.navbar-default
li li
a(href="/admin/user") Manage Users a(href="/admin/user") Manage Users
each item in nav.header
// 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.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
if item.dropdown if item.dropdown
li.dropdown(class=item.class, dropdown) li.dropdown(class=item.class, dropdown)
@ -35,9 +38,6 @@ nav.navbar.navbar-default
each child in item.dropdown each child in item.dropdown
if child.divider if child.divider
li.divider li.divider
else if child.user_email
li
div.subdued #{getUserEmail()}
else else
li li
if child.url if child.url
@ -50,7 +50,35 @@ nav.navbar.navbar-default
a(href=item.url, class=item.class) !{translate(item.text)} a(href=item.url, class=item.class) !{translate(item.text)}
else else
| !{translate(item.text)} | !{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

@ -107,6 +107,7 @@ block requirejs
window.csrfToken = "!{csrfToken}"; window.csrfToken = "!{csrfToken}";
window.anonymous = #{anonymous}; window.anonymous = #{anonymous};
window.maxDocLength = #{maxDocLength}; window.maxDocLength = #{maxDocLength};
window.trackChangesEnabled = #{trackChangesEnabled};
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = { window.requirejs = {
"paths" : { "paths" : {

View file

@ -50,7 +50,7 @@ aside.chat(
.new-message .new-message
textarea( textarea(
placeholder="#{translate('your_message')}...", placeholder=translate('your_message')+"...",
on-enter="sendMessage()", on-enter="sendMessage()",
ng-model="newMessageContent", ng-model="newMessageContent",
ng-click="resetUnreadMessages()" ng-click="resetUnreadMessages()"

View file

@ -17,7 +17,9 @@ div.full-size(
'rp-state-current-file-mini': (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-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\ 'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\
'rp-size-expanded': ui.reviewPanelOpen\ 'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\
'rp-loading-threads': reviewPanel.loadingThreads\
}" }"
) )
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening") .loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
@ -51,12 +53,12 @@ div.full-size(
syntax-validation="settings.syntaxValidation", syntax-validation="settings.syntaxValidation",
review-panel="reviewPanel", review-panel="reviewPanel",
events-bridge="reviewPanelEventsBridge" events-bridge="reviewPanelEventsBridge"
track-changes-enabled="trackChangesFeatureFlag", track-changes-enabled="project.features.trackChangesVisible",
track-new-changes= "reviewPanel.trackNewChanges", track-changes= "editor.trackChanges",
changes-tracker="reviewPanel.changesTracker",
doc-id="editor.open_doc_id" doc-id="editor.open_doc_id"
renderer-data="reviewPanel.rendererData"
) )
include ./review-panel include ./review-panel
.ui-layout-east .ui-layout-east
@ -68,7 +70,7 @@ div.full-size(
ng-controller="PdfSynctexController" ng-controller="PdfSynctexController"
) )
a.btn.btn-default.btn-xs( a.btn.btn-default.btn-xs(
tooltip="#{translate('go_to_code_location_in_pdf')}" tooltip=translate('go_to_code_location_in_pdf')
tooltip-placement="right" tooltip-placement="right"
tooltip-append-to-body="true" tooltip-append-to-body="true"
ng-click="syncToPdf()" ng-click="syncToPdf()"
@ -76,7 +78,7 @@ div.full-size(
i.fa.fa-long-arrow-right i.fa.fa-long-arrow-right
br br
a.btn.btn-default.btn-xs( a.btn.btn-default.btn-xs(
tooltip-html="'#{translate('go_to_pdf_location_in_code')}'" tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
tooltip-placement="right" tooltip-placement="right"
tooltip-append-to-body="true" tooltip-append-to-body="true"
ng-click="syncToCode()" ng-click="syncToCode()"
@ -88,4 +90,4 @@ div.full-size(
ng-show="ui.view == 'pdf'" ng-show="ui.view == 'pdf'"
) )
include ./pdf include ./pdf

View file

@ -3,21 +3,21 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a( a(
href, href,
ng-click="openNewDocModal()", ng-click="openNewDocModal()",
tooltip-html="'#{translate('new_file').replace(' ', '<br>')}'", tooltip-html="'"+translate('new_file').replace(' ', '<br>')+"'",
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-file i.fa.fa-file
a( a(
href, href,
ng-click="openNewFolderModal()", ng-click="openNewFolderModal()",
tooltip-html="'#{translate('new_folder').replace(' ', '<br>')}'", tooltip-html="'"+translate('new_folder').replace(' ', '<br>')+"'",
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-folder i.fa.fa-folder
a( a(
href, href,
ng-click="openUploadFileModal()", ng-click="openUploadFileModal()",
tooltip="#{translate('upload')}", tooltip=translate('upload'),
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-upload i.fa.fa-upload
@ -26,7 +26,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a( a(
href, href,
ng-click="startRenamingSelected()", ng-click="startRenamingSelected()",
tooltip="#{translate('rename')}", tooltip=translate('rename'),
tooltip-placement="bottom", tooltip-placement="bottom",
ng-show="multiSelectedCount == 0" ng-show="multiSelectedCount == 0"
) )
@ -34,7 +34,7 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
a( a(
href, href,
ng-click="openDeleteModalForSelected()", ng-click="openDeleteModalForSelected()",
tooltip="#{translate('delete')}", tooltip=translate('delete'),
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true" tooltip-append-to-body="true"
) )
@ -431,4 +431,4 @@ script(type='text/ng-template', id='invalidFileNameModalTemplate')
.modal-footer .modal-footer
button.btn.btn-default( button.btn.btn-default(
ng-click="$close()" ng-click="$close()"
) #{translate('ok')} ) #{translate('ok')}

View file

@ -45,7 +45,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
ng-if="permissions.admin", ng-if="permissions.admin",
href='#', href='#',
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip="#{translate('rename')}", tooltip=translate('rename'),
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="startRenaming()", ng-click="startRenaming()",
ng-show="!state.renaming" ng-show="!state.renaming"
@ -71,7 +71,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi( span.online-user.online-user-multi(
dropdown-toggle, dropdown-toggle,
tooltip="#{translate('connected_users')}", tooltip=translate('connected_users'),
tooltip-placement="left" tooltip-placement="left"
) )
strong {{ onlineUsersArray.length }} strong {{ onlineUsersArray.length }}
@ -87,7 +87,7 @@ header.toolbar.toolbar-header.toolbar-with-labels(
a.btn.btn-full-height( a.btn.btn-full-height(
href, href,
ng-if="trackChangesFeatureFlag", ng-if="project.features.trackChangesVisible",
ng-class="{ active: ui.reviewPanelOpen }" ng-class="{ active: ui.reviewPanelOpen }"
ng-click="toggleReviewPanel()" ng-click="toggleReviewPanel()"
) )
@ -121,4 +121,4 @@ header.toolbar.toolbar-header.toolbar-with-labels(
span.label.label-info( span.label.label-info(
ng-show="unreadMessages > 0" ng-show="unreadMessages > 0"
) {{ unreadMessages }} ) {{ unreadMessages }}
p.toolbar-label #{translate("chat")} p.toolbar-label #{translate("chat")}

View file

@ -24,7 +24,7 @@ aside#left-menu.full-size(
| PDF | PDF
div.link-disabled( div.link-disabled(
ng-if="!pdf.url" ng-if="!pdf.url"
tooltip="#{translate('please_compile_pdf_before_download')}" tooltip=translate('please_compile_pdf_before_download')
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-file-pdf-o.fa-2x i.fa.fa-file-pdf-o.fa-2x
@ -47,7 +47,7 @@ aside#left-menu.full-size(
a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()") a(href, ng-if="pdf.url" ,ng-click="openWordCountModal()")
i.fa.fa-fw.fa-eye i.fa.fa-fw.fa-eye
span &nbsp;&nbsp; #{translate("word_count")} span &nbsp;&nbsp; #{translate("word_count")}
a.link-disabled(href, ng-if="!pdf.url" , tooltip="#{translate('please_compile_pdf_before_word_count')}") a.link-disabled(href, ng-if="!pdf.url" , tooltip=translate('please_compile_pdf_before_word_count'))
i.fa.fa-fw.fa-eye i.fa.fa-fw.fa-eye
span.link-disabled &nbsp;&nbsp; #{translate("word_count")} span.link-disabled &nbsp;&nbsp; #{translate("word_count")}
@ -200,7 +200,7 @@ script(type='text/ng-template', id='wordCountModalTemplate')
span &nbsp; #{translate("loading")}... span &nbsp; #{translate("loading")}...
div.pdf-disabled( div.pdf-disabled(
ng-if="!pdf.url" ng-if="!pdf.url"
tooltip="#{translate('please_compile_pdf_before_word_count')}" tooltip=translate('please_compile_pdf_before_word_count')
tooltip-placement="bottom" tooltip-placement="bottom"
) )
div(ng-if="!status.loading") div(ng-if="!status.loading")

View file

@ -2,7 +2,7 @@ div.full-size.pdf(ng-controller="PdfController")
.toolbar.toolbar-tall .toolbar.toolbar-tall
.btn-group( .btn-group(
dropdown, dropdown,
tooltip-html="'#{translate('recompile_pdf')} <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'" tooltip-html="'"+translate('recompile_pdf')+" <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
tooltip-class="keyboard-tooltip" tooltip-class="keyboard-tooltip"
tooltip-popup-delay="500" tooltip-popup-delay="500"
tooltip-append-to-body="true" tooltip-append-to-body="true"
@ -53,7 +53,7 @@ div.full-size.pdf(ng-controller="PdfController")
href href
ng-click="stop()" ng-click="stop()"
ng-show="pdf.compiling", ng-show="pdf.compiling",
tooltip="#{translate('stop_compile')}" tooltip=translate('stop_compile')
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-stop() i.fa.fa-stop()
@ -61,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
href href
ng-click="toggleLogs()" ng-click="toggleLogs()"
ng-class="{ 'active': shouldShowLogs == true }" ng-class="{ 'active': shouldShowLogs == true }"
tooltip="#{translate('logs_and_output_files')}" tooltip=translate('logs_and_output_files')
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-file-text-o i.fa.fa-file-text-o
@ -77,7 +77,7 @@ div.full-size.pdf(ng-controller="PdfController")
ng-href="{{pdf.downloadUrl || pdf.url}}" ng-href="{{pdf.downloadUrl || pdf.url}}"
target="_blank" target="_blank"
ng-if="pdf.url" ng-if="pdf.url"
tooltip="#{translate('download_pdf')}" tooltip=translate('download_pdf')
tooltip-placement="bottom" tooltip-placement="bottom"
) )
i.fa.fa-download i.fa.fa-download
@ -87,7 +87,7 @@ div.full-size.pdf(ng-controller="PdfController")
href, href,
ng-click="switchToFlatLayout()" ng-click="switchToFlatLayout()"
ng-show="ui.pdfLayout == 'sideBySide'" ng-show="ui.pdfLayout == 'sideBySide'"
tooltip="#{translate('full_screen')}" tooltip=translate('full_screen')
tooltip-placement="bottom" tooltip-placement="bottom"
tooltip-append-to-body="true" tooltip-append-to-body="true"
) )
@ -96,7 +96,7 @@ div.full-size.pdf(ng-controller="PdfController")
href, href,
ng-click="switchToSideBySideLayout()" ng-click="switchToSideBySideLayout()"
ng-show="ui.pdfLayout == 'flat'" ng-show="ui.pdfLayout == 'flat'"
tooltip="#{translate('split_screen')}" tooltip=translate('split_screen')
tooltip-placement="bottom" tooltip-placement="bottom"
tooltip-append-to-body="true" tooltip-append-to-body="true"
) )
@ -233,7 +233,7 @@ div.full-size.pdf(ng-controller="PdfController")
.files-dropdown-container .files-dropdown-container
a.btn.btn-default.btn-sm( a.btn.btn-default.btn-sm(
href, href,
tooltip="#{translate('clear_cached_files')}", tooltip=translate('clear_cached_files'),
tooltip-placement="top", tooltip-placement="top",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="openClearCacheModal()" ng-click="openClearCacheModal()"

View file

@ -1,219 +0,0 @@
#review-panel
.review-panel-toolbar
span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = true;", ng-if="reviewPanel.trackNewChanges === false") Track Changes is
strong off
span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = false;", ng-if="reviewPanel.trackNewChanges === true") Track Changes is
strong on
review-panel-toggle(ng-model="reviewPanel.trackNewChanges")
.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]"
)
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();"
)
div(ng-if="entry.type === 'comment'")
comment-entry(
entry="entry"
users="users"
on-resolve="resolveComment(entry, entry_id)"
on-unresolve="unresolveComment(entry_id)"
on-show-thread="showThread(entry)"
on-hide-thread="hideThread(entry)"
on-delete="deleteComment(entry_id)"
on-reply="submitReply(entry, entry_id);"
on-indicator-click="toggleReviewPanel();"
)
div(ng-if="entry.type === 'add-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-overview-file(
ng-repeat="(doc_id, entries) in reviewPanel.entries"
)
.rp-overview-file-header
| {{ getFileName(doc_id) }}
.rp-entry-wrapper(
ng-repeat="(entry_id, entry) in entries | orderOverviewEntries"
)
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();"
ng-click="gotoEntry(doc_id, entry)"
)
div(ng-if="entry.type === 'comment'")
comment-entry(
entry="entry"
users="users"
on-resolve="resolveComment(entry, entry.id)"
on-unresolve="unresolveComment(entry.id)"
on-delete="deleteComment(entry.id)"
on-reply="submitReply(entry, entry_id);"
on-indicator-click="toggleReviewPanel();"
ng-click="gotoEntry(doc_id, entry)"
)
.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-header
.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-metadata
p.rp-entry-metadata-line(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }}
p.rp-entry-metadata-line {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }}
.rp-avatar(style="background-color: hsl({{ user.hue }}, 70%, 50%);") {{ user.avatar_text | limitTo : 1 }}
.rp-entry-body(ng-switch="entry.type")
span(ng-switch-when="insert") Added&nbsp;
ins.rp-content-highlight {{ entry.content }}
span(ng-switch-when="delete") Deleted&nbsp;
del.rp-content-highlight {{ entry.content }}
.rp-entry-actions
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')
div
.rp-entry-callout.rp-entry-callout-comment(ng-if="!entry.resolved")
.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-resolved': entry.resolved}"
)
.rp-comment(
ng-if="!entry.resolved || entry.showWhenResolved"
ng-repeat="comment in entry.thread"
ng-class="users[comment.user_id].isSelf ? 'rp-comment-self' : '';"
)
.rp-avatar(
ng-if="!users[comment.user_id].isSelf;"
style="background-color: hsl({{ users[comment.user_id].hue }}, 70%, 50%);"
) {{ users[comment.user_id].avatar_text | limitTo : 1 }}
.rp-comment-body(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 90%);")
p.rp-comment-content {{ comment.content }}
p.rp-comment-metadata
| {{ comment.ts | date : 'MMM d, y h:mm a' }}
| &nbsp;&bull;&nbsp;
span(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 40%);") {{ users[comment.user_id].name }}
.rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved")
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-comment-resolved-description(ng-if="entry.resolved && !entry.showWhenResolved")
div
| Comment resolved by
span(style="color: hsl({{ users[entry.resolved_data.user_id].hue }}, 70%, 40%);") {{ users[entry.resolved_data.user_id].name }}
div {{ entry.resolved_data.ts | date : 'MMM d, y h:mm a' }}
.rp-entry-actions
a.rp-entry-button(href, ng-click="onResolve();", ng-if="!entry.resolved")
i.fa.fa-check
| &nbsp;Mark as resolved
a.rp-entry-button(href, ng-click="onShowThread();", ng-if="entry.resolved && !entry.showWhenResolved")
| &nbsp;Show
a.rp-entry-button(href, ng-click="onHideThread();", ng-if="entry.resolved && entry.showWhenResolved")
| &nbsp;Hide
a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="entry.resolved")
| &nbsp;Re-open
a.rp-entry-button(href, ng-click="onDelete();", ng-if="entry.resolved")
| &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"
)
.rp-entry-actions
a.rp-entry-button(href, ng-click="cancelNewComment();")
i.fa.fa-times
| &nbsp;Cancel
a.rp-entry-button(href, ng-click="submitNewComment()")
i.fa.fa-comment
| &nbsp;Comment

View file

@ -0,0 +1,426 @@
#review-panel
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
.review-panel-toolbar
resolved-comments-dropdown(
class="rp-flex-block"
entries="reviewPanel.resolvedComments"
threads="reviewPanel.commentThreads"
resolved-ids="reviewPanel.resolvedThreadIds"
docs="docs"
on-open="refreshResolvedCommentsDropdown();"
on-unresolve="unresolveComment(threadId);"
on-delete="deleteThread(entryId, docId, 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"
disabled="!project.features.trackChanges"
on-disabled-click="openTrackChangesUpgradeModal"
)
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]"
)
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();"
on-body-click="gotoEntry(editor.open_doc_id, entry)"
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();"
on-save-edit="saveEdit(entry.thread_id, comment)"
on-delete="deleteComment(entry.thread_id, comment)"
on-body-click="gotoEntry(editor.open_doc_id, entry)"
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-save-edit="saveEdit(entry.thread_id, comment)"
on-delete="deleteComment(entry.thread_id, comment)"
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 }"
)
.rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)")
| No comments
.rp-comment-loaded
.rp-comment(
ng-repeat="comment in threads[entry.thread_id].messages track by comment.id"
)
p.rp-comment-content
span(ng-if="!comment.editing")
span.rp-entry-user(
style="color: hsl({{ comment.user.hue }}, 70%, 40%);",
) {{ comment.user.name }}:&nbsp;
span(ng-bind-html="comment.content | linky:'_blank'")
textarea.rp-comment-input(
expandable-text-area
ng-if="comment.editing"
ng-model="comment.content"
ng-keypress="saveEditOnEnter($event, comment);"
ng-blur="saveEdit(comment)"
autofocus
stop-propagation="click"
)
.rp-entry-metadata(ng-if="!comment.editing")
span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }}
span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting")
| &nbsp;&bull;&nbsp;
a(href, ng-click="startEditing(comment)") Edit
span(ng-if="threads[entry.thread_id].messages.length > 1")
| &nbsp;&bull;&nbsp;
a(href, ng-click="confirmDelete(comment)") Delete
span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting")
| Are you sure?
| &bull;&nbsp;
a(href, ng-click="doDelete(comment)") Delete
| &nbsp;&bull;&nbsp;
a(href, ng-click="cancelDelete(comment)") Cancel
.rp-loading(ng-if="threads[entry.thread_id].submitting")
i.fa.fa-spinner.fa-spin
.rp-comment-reply(ng-if="permissions.comment")
textarea.rp-comment-input(
expandable-text-area
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;
span(ng-bind-html="comment.content | linky:'_blank'")
.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, 'docId': thread.docId, '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(
expandable-text-area
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, docId, threadId);"
permissions="permissions"
)
.rp-loading(ng-if="!resolvedComments.length")
| No resolved threads.
script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 Upgrade to Track Changes
.modal-body
.teaser-video-container
video.teaser-video(autoplay, loop)
source(src="/img/teasers/track-changes/teaser-track-changes.mp4", type="video/mp4")
img(src="/img/teasers/track-changes/teaser-track-changes.gif")
h4.teaser-title See changes in your documents, live
p.small(ng-show="startedFreeTrial")
| #{translate("refresh_page_after_starting_free_trial")}
.row
.col-md-10.col-md-offset-1
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| Track any change, in real-time
li
i.fa.fa-check &nbsp;
| Review your peers' work
li
i.fa.fa-check &nbsp;
| Accept or reject each change individually
.row.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-click="startFreeTrial('track-changes')"
) Try it for free
.modal-footer()
button.btn.btn-default(
ng-click="cancel()"
)
span #{translate("close")}

View file

@ -38,7 +38,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.col-xs-1 .col-xs-1
a( a(
href href
tooltip="#{translate('remove_collaborator')}" tooltip=translate('remove_collaborator')
tooltip-placement="bottom" tooltip-placement="bottom"
ng-click="removeMember(member)" ng-click="removeMember(member)"
) )
@ -55,7 +55,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.col-xs-1 .col-xs-1
a( a(
href href
tooltip="#{translate('revoke_invite')}" tooltip=translate('revoke_invite')
tooltip-placement="bottom" tooltip-placement="bottom"
ng-click="revokeInvite(invite)" ng-click="revokeInvite(invite)"
) )
@ -66,7 +66,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.form-group .form-group
tags-input( tags-input(
template="shareTagTemplate" template="shareTagTemplate"
placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}" placeholder=settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'
ng-model="inputs.contacts" ng-model="inputs.contacts"
focus-on="open" focus-on="open"
display-property="display" display-property="display"
@ -144,6 +144,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
span(ng-switch="state.errorReason") span(ng-switch="state.errorReason")
span(ng-switch-when="cannot_invite_non_user") span(ng-switch-when="cannot_invite_non_user")
| #{translate("cannot_invite_non_user")} | #{translate("cannot_invite_non_user")}
span(ng-switch-when="cannot_invite_self")
| #{translate("cannot_invite_self")}
span(ng-switch-default) span(ng-switch-default)
| #{translate("generic_something_went_wrong")} | #{translate("generic_something_went_wrong")}
button.btn.btn-default( button.btn.btn-default(

View file

@ -20,12 +20,12 @@ block content
form.form( form.form(
name="acceptForm", name="acceptForm",
method="POST", method="POST",
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept" action="/project/"+invite.projectId+"/invite/token/"+invite.token+"/accept"
) )
input(name='_csrf', type='hidden', value=csrfToken) input(name='_csrf', type='hidden', value=csrfToken)
input(name='token', type='hidden', value="#{invite.token}") input(name='token', type='hidden', value=invite.token)
.form-group.text-center .form-group.text-center
button.btn.btn-lg.btn-primary(type="submit") button.btn.btn-lg.btn-primary(type="submit")
| #{translate("join_project")} | #{translate("join_project")}
.form-group.text-center .form-group.text-center

View file

@ -73,4 +73,4 @@ block content
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8 .col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/empty-project-list include ./list/empty-project-list
include ./list/modals include ./list/modals

View file

@ -6,7 +6,7 @@
form.project-search.form-horizontal(role="form") form.project-search.form-horizontal(role="form")
.form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12
input.form-control.col-md-7.col-xs-12( input.form-control.col-md-7.col-xs-12(
placeholder="#{translate('search_projects')}…", placeholder=translate('search_projects')+"…",
autofocus='autofocus', autofocus='autofocus',
ng-model="searchText.value", ng-model="searchText.value",
focus-on='search:clear', focus-on='search:clear',
@ -25,7 +25,7 @@
.btn-group(ng-hide="selectedProjects.length < 1") .btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default( a.btn.btn-default(
href, href,
tooltip="#{translate('download')}", tooltip=translate('download'),
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="downloadSelectedProjects()" ng-click="downloadSelectedProjects()"
@ -33,7 +33,7 @@
i.fa.fa-cloud-download i.fa.fa-cloud-download
a.btn.btn-default( a.btn.btn-default(
href, href,
tooltip="#{translate('delete')}", tooltip=translate('delete'),
tooltip-placement="bottom", tooltip-placement="bottom",
tooltip-append-to-body="true", tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()" ng-click="openArchiveProjectsModal()"
@ -45,7 +45,7 @@
href, href,
data-toggle="dropdown", data-toggle="dropdown",
dropdown-toggle, dropdown-toggle,
tooltip="#{translate('add_to_folders')}", tooltip=translate('add_to_folders'),
tooltip-append-to-body="true", tooltip-append-to-body="true",
tooltip-placement="bottom" tooltip-placement="bottom"
) )

View file

@ -24,7 +24,7 @@ block content
.row .row
.col-md-8.col-md-offset-2.bonus-banner .col-md-8.col-md-offset-2.bonus-banner
.title .title
a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url=#{encodeURIComponent(buildReferalUrl("t"))}&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet a(href='https://twitter.com/share?text=is%20trying%20out%20the%20online%20LaTeX%20Editor%20ShareLaTeX&url='+encodeURIComponent(buildReferalUrl("t"))+'&counturl=https://www.sharelatex.com', target="_blank").twitter Tweet
.row .row
.col-md-8.col-md-offset-2.bonus-banner .col-md-8.col-md-offset-2.bonus-banner
@ -34,12 +34,12 @@ block content
.row .row
.col-md-8.col-md-offset-2.bonus-banner .col-md-8.col-md-offset-2.bonus-banner
.title .title
a(href="https://plus.google.com/share?url=#{encodeURIComponent(buildReferalUrl('gp'))}", onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")} a(href="https://plus.google.com/share?url="+encodeURIComponent(buildReferalUrl('gp')), onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;").google-plus #{translate("share_us_on_googleplus")}
.row .row
.col-md-8.col-md-offset-2.bonus-banner .col-md-8.col-md-offset-2.bonus-banner
.title .title
a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. #{encodeURIComponent(buildReferalUrl("e"))}', title='Share by Email').email #{translate("email_us_to_your_friends")} a(href='mailto:?subject=Online LaTeX editor you may like &body=Hey, I have been using the online LaTeX editor ShareLaTeX recently and thought you might like to check it out. '+encodeURIComponent(buildReferalUrl("e")), title='Share by Email').email #{translate("email_us_to_your_friends")}
.row .row
.col-md-8.col-md-offset-2.bonus-banner .col-md-8.col-md-offset-2.bonus-banner
@ -58,9 +58,9 @@ block content
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;") .col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;")
- for (var i = 0; i <= 10; i++) { - for (var i = 0; i <= 10; i++) {
- if (refered_user_count == i) - if (refered_user_count == i)
.number(style="left: #{i}0%").active #{i} .number(style="left: "+i+"0%").active #{i}
- else - else
.number(style="left: #{i}0%") #{i} .number(style="left: "+i+"0%") #{i}
- } - }
.row.ab-bonus .row.ab-bonus
@ -68,7 +68,7 @@ block content
.progress .progress
- if (refered_user_count == 0) - if (refered_user_count == 0)
div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")} div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")}
.progress-bar.progress-bar-info(style="width: #{refered_user_count}0%") .progress-bar.progress-bar-info(style="width: "+refered_user_count+"0%")
.row.ab-bonus .row.ab-bonus
.col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;") .col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 70px;")

View file

@ -19,11 +19,11 @@ block content
.row-fluid .row-fluid
table.table table.table
-each project in projects each project in projects
tr tr
- project_id = project._id.toString() - project_id = project._id.toString()
td(width="50%") #{project.name} td(width="50%") #{project.name}
td(width="25%") td(width="25%")
a.btn(href="/project/#{project_id}/zip") Download latest version as Zip a.btn(href="/project/"+project_id+"/zip") Download latest version as Zip
include general/small-footer include general/small-footer

View file

@ -5,4 +5,4 @@ script(type='text/ng-template', id='scribtexModalTemplate')
p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks. p ScribTeX has moved to <strong>https://scribtex.sharelatex.com</strong>. Please update your bookmarks.
p(style="text-align: center") You can find the page you were looking for here: p(style="text-align: center") You can find the page you were looking for here:
p(style="text-align: center") p(style="text-align: center")
a(href="https://scribtex.sharelatex.com#{scribtexPath}", style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath} a(href="https://scribtex.sharelatex.com"+scribtexPath, style="font-size: 16px") https://scribtex.sharelatex.com#{scribtexPath}

View file

@ -1,8 +1,8 @@
- if (typeof(sentrySrc) != "undefined") - if (typeof(sentrySrc) != "undefined")
- if (sentrySrc.match(/^([a-z]+:)?\/\//i)) - if (sentrySrc.match(/^([a-z]+:)?\/\//i))
script(src="#{sentrySrc}") script(src=sentrySrc)
- else - else
script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false})) script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false}))
- if (typeof(sentrySrc) != "undefined") - if (typeof(sentrySrc) != "undefined")
script(type="text/javascript"). script(type="text/javascript").
if (typeof(Raven) != "undefined" && Raven.config) { if (typeof(Raven) != "undefined" && Raven.config) {

View file

@ -12,7 +12,7 @@ block scripts
mixin printPlan(plan) mixin printPlan(plan)
-if (!plan.hideFromUsers) -if (!plan.hideFromUsers)
tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)") tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan), ng-show="shouldShowPlan(plan.planCode)")
td td
strong #{plan.name} strong #{plan.name}
td {{refreshPrice(plan.planCode)}} td {{refreshPrice(plan.planCode)}}
@ -22,18 +22,18 @@ mixin printPlan(plan)
| {{prices[plan.planCode]}} / #{translate("month")} | {{prices[plan.planCode]}} / #{translate("month")}
td td
-if (subscription.state == "free-trial") -if (subscription.state == "free-trial")
a(href="/user/subscription/new?planCode=#{plan.planCode}").btn.btn-success #{translate("subscribe_to_this_plan")} a(href="/user/subscription/new?planCode="+plan.planCode).btn.btn-success #{translate("subscribe_to_this_plan")}
-else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0]) -else if (typeof(subscription.planCode) != "undefined" && plan.planCode == subscription.planCode.split("_")[0])
button.btn.disabled #{translate("your_plan")} button.btn.disabled #{translate("your_plan")}
-else -else
form form
input(type="hidden", ng-model="plan_code", name="plan_code", value="#{plan.planCode}") input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success
mixin printPlans(plans) mixin printPlans(plans)
-each plan in plans each plan in plans
mixin printPlan(plan) +printPlan(plan)
block content block content
.content.content-alt(ng-cloak) .content.content-alt(ng-cloak)
@ -46,7 +46,7 @@ block content
| &nbsp; | &nbsp;
| #{translate("your_billing_details_were_saved")} | #{translate("your_billing_details_were_saved")}
.card(ng-if="view == 'overview'") .card(ng-if="view == 'overview'")
.page-header(x-current-plan="#{subscription.planCode}") .page-header(x-current-plan=subscription.planCode)
h1 #{translate("your_subscription")} h1 #{translate("your_subscription")}
- if (subscription && user._id+'' == subscription.admin_id+'') - if (subscription && user._id+'' == subscription.admin_id+'')
@ -97,9 +97,9 @@ block content
th !{translate("name")} th !{translate("name")}
th !{translate("price")} th !{translate("price")}
th th
mixin printPlans(plans.studentAccounts) +printPlans(plans.studentAccounts)
mixin printPlans(plans.individualMonthlyPlans) +printPlans(plans.individualMonthlyPlans)
mixin printPlans(plans.individualAnnualPlans) +printPlans(plans.individualAnnualPlans)
each groupSubscription in groupSubscriptions each groupSubscription in groupSubscriptions
@ -107,7 +107,7 @@ block content
div div
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})} p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
span span
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")} button.btn.btn-danger(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'') -if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
div div

View file

@ -164,7 +164,7 @@ block content
ng-change="updateCountry()" ng-change="updateCountry()"
required required
) )
mixin countries_options() +countries_options()
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }} span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
if (showVatField) if (showVatField)

View file

@ -6,7 +6,7 @@ block content
.container(ng-controller="AnnualUpgradeController") .container(ng-controller="AnnualUpgradeController")
.row(ng-cloak) .row(ng-cloak)
.col-md-6.col-md-offset-3 .col-md-6.col-md-offset-3
.card(ng-init="planName = #{JSON.stringify(planName)}") .card(ng-init="planName = "+JSON.stringify(planName))
.page-header .page-header
h1.text-centered #{translate("move_to_annual_billing")} h1.text-centered #{translate("move_to_annual_billing")}
div(ng-hide="upgradeComplete") div(ng-hide="upgradeComplete")

View file

@ -2,7 +2,7 @@
span(ng-controller="TranslationsPopupController", ng-cloak) span(ng-controller="TranslationsPopupController", ng-cloak)
.translations-message(ng-hide="hidei18nNotification") .translations-message(ng-hide="hidei18nNotification")
a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})} a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})}
img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png")) img(src=buildImgPath("flags/24/" + recomendSubdomain.lngCode + ".png"))
button(ng-click="dismiss()").close.pull-right button(ng-click="dismiss()").close.pull-right
span(aria-hidden="true") &times; span(aria-hidden="true") &times;
span.sr-only #{translate("close")} span.sr-only #{translate("close")}

View file

@ -36,7 +36,7 @@ block content
placeholder="email@example.com" placeholder="email@example.com"
required, required,
ng-model="email", ng-model="email",
ng-init="email = #{JSON.stringify(email)}", ng-init="email = "+JSON.stringify(email),
ng-model-options="{ updateOn: 'blur' }", ng-model-options="{ updateOn: 'blur' }",
disabled disabled
) )

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