Merge branch 'master' into node-4.2

This commit is contained in:
Henry Oswald 2016-03-14 16:20:39 +00:00
commit f4cbcc22ba
182 changed files with 56804 additions and 3311 deletions

View file

@ -95,10 +95,10 @@ module.exports = (grunt) ->
paths:
"moment": "libs/moment-2.9.0"
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"libs/pdf": "libs/pdfjs-1.0.1040/pdf"
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
shim:
"libs/pdf":
deps: ["libs/pdfjs-1.0.1040/compatibility"]
deps: ["libs/pdfjs-1.3.91/compatibility"]
skipDirOptimize: true
modules: [

View file

@ -3,8 +3,12 @@ logger = require 'logger-sharelatex'
logger.initialize("web-sharelatex")
logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user
logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project
if Settings.sentry?.dsn?
logger.initializeErrorReporting(Settings.sentry.dsn)
metrics = require("metrics-sharelatex")
metrics.initialize("web")
metrics.memory.monitor(logger)
Server = require("./app/js/infrastructure/Server")
Errors = require "./app/js/errors"
@ -15,6 +19,10 @@ argv = require("optimist")
.argv
Server.app.use (error, req, res, next) ->
if error?.code is 'EBADCSRFTOKEN'
logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf"
res.sendStatus(403)
return
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
res.statusCode = error.status or 500
if res.statusCode == 500

View file

@ -8,13 +8,16 @@ querystring = require('querystring')
Url = require("url")
Settings = require "settings-sharelatex"
basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
email = req.body?.email?.toLowerCase()
password = req.body?.password
redir = Url.parse(req.body?.redir or "/project").path
AuthenticationController.doLogin req.body, req, res, next
doLogin: (options, req, res, next) ->
email = options.email?.toLowerCase()
password = options.password
redir = Url.parse(options.redir or "/project").path
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
if !isAllowed
logger.log email:email, "too many login requests"
@ -26,17 +29,18 @@ module.exports = AuthenticationController =
AuthenticationManager.authenticate email: email, password, (error, user) ->
return next(error) if error?
if user?
UserHandler.setupLoginData user, ->
LoginRateLimiter.recordSuccessfulLogin email
AuthenticationController._recordSuccessfulLogin user._id
AuthenticationController.establishUserSession req, user, (error) ->
return next(error) if error?
req.session.justLoggedIn = true
logger.log email: email, user_id: user._id.toString(), "successful log in"
res.send redir: redir
res.json redir: redir
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
res.send message:
res.json message:
text: req.i18n.translate("email_or_password_wrong_try_again"),
type: 'error'

View file

@ -1,4 +1,3 @@
Settings = require 'settings-sharelatex'
User = require("../../models/User").User
{db, ObjectId} = require("../../infrastructure/mongojs")
crypto = require 'crypto'

View file

@ -4,15 +4,13 @@ logger = require("logger-sharelatex")
_ = require("underscore")
ErrorController = require "../Errors/ErrorController"
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps"]
module.exports = BlogController =
getPage: (req, res, next)->
url = req.url?.toLowerCase()
blogUrl = "#{settings.apis.blog.url}#{url}"
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps"]
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps", ".gif"]
shouldProxy = _.find extensionsToProxy, (extension)->
url.indexOf(extension) != -1

View file

@ -58,8 +58,8 @@ module.exports = ClsiManager =
return outputFiles
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
_buildRequest: (project_id, settingsOverride={}, callback = (error, request) ->) ->
Project.findById project_id, {compiler: 1, rootDoc_id: 1}, (error, project) ->
_buildRequest: (project_id, options={}, callback = (error, request) ->) ->
Project.findById project_id, {compiler: 1, rootDoc_id: 1, imageName: 1}, (error, project) ->
return callback(error) if error?
return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
@ -82,7 +82,7 @@ module.exports = ClsiManager =
content: doc.lines.join("\n")
if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
rootResourcePath = path
if settingsOverride.rootDoc_id? and doc._id.toString() == settingsOverride.rootDoc_id.toString()
if options.rootDoc_id? and doc._id.toString() == options.rootDoc_id.toString()
rootResourcePathOverride = path
rootResourcePath = rootResourcePathOverride if rootResourcePathOverride?
@ -101,7 +101,9 @@ module.exports = ClsiManager =
compile:
options:
compiler: project.compiler
timeout: settingsOverride.timeout
timeout: options.timeout
imageName: project.imageName
draft: !!options.draft
rootResourcePath: rootResourcePath
resources: resources
}
@ -110,8 +112,11 @@ module.exports = ClsiManager =
ClsiManager._buildRequest project_id, options, (error, req) ->
compilerUrl = ClsiManager._getCompilerUrl(options?.compileGroup)
filename = file || req?.compile?.rootResourcePath
wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}"
if req.compile.options.imageName?
wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}"
request.get {
url: "#{compilerUrl}/project/#{project_id}/wordcount?file=#{filename}"
url: wordcount_url
}, (error, response, body) ->
return callback(error) if error?
if 200 <= response.statusCode < 300

View file

@ -25,6 +25,8 @@ module.exports = CompileController =
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
logger.log {options, project_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, output, limits) ->
return next(error) if error?

View file

@ -1,6 +1,5 @@
request = require 'request'
request = request.defaults()
async = require 'async'
settings = require 'settings-sharelatex'
_ = require 'underscore'
async = require 'async'
@ -116,7 +115,7 @@ module.exports = DocumentUpdaterHandler =
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
setDocument : (project_id, doc_id, docLines, source, callback = (error) ->)->
setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)->
timer = new metrics.Timer("set-document")
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
body =
@ -124,7 +123,8 @@ module.exports = DocumentUpdaterHandler =
json:
lines: docLines
source: source
logger.log project_id:project_id, doc_id: doc_id, source: source, "setting doc in document updater"
user_id: user_id
logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater"
request.post body, (error, res, body)->
timer.done()
if error?

View file

@ -6,15 +6,20 @@ module.exports =
getDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
doc_id = req.params.doc_id
plain = req?.query?.plain == 'true'
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev) ->
if error?
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
return next(error)
res.type "json"
res.send JSON.stringify {
lines: lines
}
if plain
res.type "text/plain"
res.send lines.join('\n')
else
res.type "json"
res.send JSON.stringify {
lines: lines
}
setDocument: (req, res, next = (error) ->) ->
project_id = req.params.Project_id

View file

@ -13,8 +13,8 @@ LockManager = require("../../infrastructure/LockManager")
_ = require('underscore')
module.exports = EditorController =
setDoc: (project_id, doc_id, docLines, source, callback = (err)->)->
DocumentUpdaterHandler.setDocument project_id, doc_id, docLines, source, (err)=>
setDoc: (project_id, doc_id, user_id, docLines, source, callback = (err)->)->
DocumentUpdaterHandler.setDocument project_id, doc_id, user_id, docLines, source, (err)=>
logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated"
DocumentUpdaterHandler.flushDocToMongo project_id, doc_id, callback
@ -33,10 +33,12 @@ module.exports = EditorController =
logger.log {project_id, folder_id, docName, source}, "sending new doc to project"
Metrics.inc "editor.add-doc"
ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, (err, doc, folder_id)=>
if err?
logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock"
return callback(err)
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source)
callback(err, doc)
addFile: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
LockManager.getLock project_id, (err)->
if err?
@ -46,20 +48,20 @@ module.exports = EditorController =
LockManager.releaseLock project_id, ->
callback(error, file)
addFileWithoutLock: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
fileName = fileName.trim()
logger.log {project_id, folder_id, fileName, path}, "sending new file to project"
Metrics.inc "editor.add-file"
ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
if err?
logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock"
return callback(err)
EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source)
callback(err, fileRef)
replaceFile: (project_id, file_id, fsPath, source, callback = (error) ->)->
ProjectEntityHandler.replaceFile project_id, file_id, fsPath, callback
addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)->
LockManager.getLock project_id, (err)->
if err?
@ -74,6 +76,9 @@ module.exports = EditorController =
logger.log {project_id, folder_id, folderName, source}, "sending new folder to project"
Metrics.inc "editor.add-folder"
ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
if err?
logger.err err:err, project_id:project_id, folder_id:folder_id, folderName:folderName, "error adding folder without lock"
return callback(err)
@p.notifyProjectUsersOfNewFolder project_id, folder_id, folder, (error) ->
callback error, folder
@ -90,6 +95,9 @@ module.exports = EditorController =
mkdirpWithoutLock: (project_id, path, callback)->
logger.log project_id:project_id, path:path, "making directories if they don't exist"
ProjectEntityHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=>
if err?
logger.err err:err, project_id:project_id, path:path, "error mkdirp without lock"
return callback(err)
self = @
jobs = _.map newFolders, (folder, index)->
return (cb)->
@ -109,7 +117,10 @@ module.exports = EditorController =
deleteEntityWithoutLock: (project_id, entity_id, entityType, source, callback)->
logger.log {project_id, entity_id, entityType, source}, "start delete process of entity"
Metrics.inc "editor.delete-entity"
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, =>
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity"
return callback(err)
logger.log project_id:project_id, entity_id:entity_id, entityType:entityType, "telling users entity has been deleted"
EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source)
if callback?
@ -143,19 +154,28 @@ module.exports = EditorController =
newName = sanitize.escape(newName)
Metrics.inc "editor.rename-entity"
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, =>
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, ->
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity"
return callback(err)
if newName.length > 0
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
callback?()
#
moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
Metrics.inc "editor.move-entity"
ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
if err?
logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity"
return callback(err)
EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
callback?()
renameProject: (project_id, newName, callback = (err) ->) ->
ProjectDetailsHandler.renameProject project_id, newName, =>
ProjectDetailsHandler.renameProject project_id, newName, ->
if err?
logger.err err:err, project_id:project_id, newName:newName, "error renaming project"
return callback(err)
EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', newName
callback()

View file

@ -67,11 +67,16 @@ module.exports = EditorHttpController =
project_id = req.params.Project_id
name = req.body.name
parent_folder_id = req.body.parent_folder_id
logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project"
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) ->
return next(error) if error?
res.json doc
if error == "project_has_to_many_files"
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
else if error?
next(error)
else
res.json doc
addFolder: (req, res, next) ->
project_id = req.params.Project_id
@ -80,8 +85,12 @@ module.exports = EditorHttpController =
if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
return next(error) if error?
res.json doc
if error == "project_has_to_many_files"
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
else if error?
next(error)
else
res.json doc
renameEntity: (req, res, next) ->
project_id = req.params.Project_id

View file

@ -1,5 +1,4 @@
_ = require('underscore')
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
settings = require("settings-sharelatex")
@ -15,8 +14,6 @@ templates.registered =
<p><a href="<%= setNewPasswordUrl %>">Click here to set your password and log in.</a></p>
<p>Once you have reset your password you can <a href="#{settings.siteUrl}/login">log in here</a>.</p>
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
"""

View file

@ -1,8 +1,9 @@
logger = require('logger-sharelatex')
metrics = require('../../infrastructure/Metrics')
Settings = require('settings-sharelatex')
metrics = require("../../infrastructure/Metrics")
nodemailer = require("nodemailer")
sesTransport = require('nodemailer-ses-transport')
_ = require("underscore")
if Settings.email? and Settings.email.fromAddress?
defaultFromAddress = Settings.email.fromAddress
@ -15,15 +16,24 @@ client =
logger.log options:options, "Would send email if enabled."
callback()
if Settings.email?
if Settings.email.transport? and Settings.email.parameters?
nm_client = nodemailer.createTransport( Settings.email.transport, Settings.email.parameters )
if nm_client
client = nm_client
else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
else
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
if Settings?.email?.parameters?.AWSAccessKeyID?
logger.log "using aws ses for email"
nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters))
else if Settings?.email?.parameters?
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth")
logger.log "using smtp for email"
nm_client = nodemailer.createTransport(smtp)
else
nm_client = client
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
if nm_client?
client = nm_client
else
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
module.exports =
sendEmail : (options, callback = (error) ->)->
@ -42,4 +52,3 @@ module.exports =
else
logger.log "Message sent to #{options.to}"
callback(err)

View file

@ -6,24 +6,32 @@ settings = require("settings-sharelatex")
oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5
module.exports =
module.exports = FileStoreHandler =
uploadFileFromDisk: (project_id, file_id, fsPath, callback)->
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
readStream = fs.createReadStream(fsPath)
opts =
method: "post"
uri: @_buildUrl(project_id, file_id)
timeout:fiveMinsInMs
writeStream = request(opts)
readStream.pipe writeStream
writeStream.on "end", callback
readStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
callback err
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
fs.lstat fsPath, (err, stat)->
if err?
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file"
callback(err)
if !stat.isFile()
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining"
return callback(new Error("can not upload symlink"))
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
readStream = fs.createReadStream(fsPath)
opts =
method: "post"
uri: FileStoreHandler._buildUrl(project_id, file_id)
timeout:fiveMinsInMs
writeStream = request(opts)
readStream.pipe writeStream
writeStream.on "end", callback
readStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
callback err
writeStream.on "error", (err)->
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
callback err
getFileStream: (project_id, file_id, query, callback)->
logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store"

View file

@ -0,0 +1,16 @@
NotificationsHandler = require("./NotificationsHandler")
module.exports =
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback

View file

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

View file

@ -0,0 +1,52 @@
settings = require("settings-sharelatex")
request = require("request")
logger = require("logger-sharelatex")
oneSecond = 1000
module.exports =
getUserNotifications: (user_id, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
json: true
timeout: oneSecond
request.get opts, (err, res, unreadNotifications)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
logger.err err:err, "something went wrong getting notifications"
callback(null, [])
else
if !unreadNotifications?
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
timeout: oneSecond
json: {
key:key
messageOpts:messageOpts
templateKey:templateKey
}
logger.log opts:opts, "creating notification for user"
request.post opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
timeout: oneSecond
json: {
key:key
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
request.del opts, callback
markAsRead: (user_id, notification_id, callback)->
opts =
uri: "#{settings.apis.notifications.url}/user/#{user_id}/notification/#{notification_id}"
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
request.del opts, callback

View file

@ -1,5 +1,7 @@
PasswordResetHandler = require("./PasswordResetHandler")
RateLimiter = require("../../infrastructure/RateLimiter")
AuthenticationController = require("../Authentication/AuthenticationController")
UserGetter = require("../User/UserGetter")
logger = require "logger-sharelatex"
module.exports =
@ -37,14 +39,19 @@ module.exports =
title:"set_password"
passwordResetToken: req.session.resetToken
setNewUserPassword: (req, res)->
setNewUserPassword: (req, res, next)->
{passwordResetToken, password} = req.body
if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0
return res.sendStatus 400
delete req.session.resetToken
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) ->
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
return next(err) if err?
if found
res.sendStatus 200
if req.body.login_after
UserGetter.getUser user_id, {email: 1}, (err, user) ->
return next(err) if err?
AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
else
res.sendStatus 200
else
res.send 404, {message: req.i18n.translate("password_reset_token_expired")}
res.sendStatus 404

View file

@ -23,11 +23,11 @@ module.exports =
return callback(error) if error?
callback null, true
setNewUserPassword: (token, password, callback = (error, found) ->)->
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)->
if err then return callback(err)
if !user_id?
return callback null, false
return callback null, false, null
AuthenticationManager.setUserPassword user_id, password, (err) ->
if err then return callback(err)
callback null, true
callback null, true, user_id

View file

@ -9,6 +9,7 @@ Project = require('../../models/Project').Project
User = require('../../models/User').User
TagsHandler = require("../Tags/TagsHandler")
SubscriptionLocator = require("../Subscription/SubscriptionLocator")
NotificationsHandler = require("../Notifications/NotificationsHandler")
LimitationsManager = require("../Subscription/LimitationsManager")
_ = require("underscore")
Settings = require("settings-sharelatex")
@ -51,7 +52,7 @@ module.exports = ProjectController =
deleteProject: (req, res) ->
project_id = req.params.Project_id
forever = req.query?.forever?
logger.log project_id: project_id, forever: forever, "received request to delete project"
logger.log project_id: project_id, forever: forever, "received request to archive project"
if forever
doDelete = projectDeleter.deleteProject
@ -125,6 +126,8 @@ module.exports = ProjectController =
async.parallel {
tags: (cb)->
TagsHandler.getAllTags user_id, cb
notifications: (cb)->
NotificationsHandler.getUserNotifications user_id, cb
projects: (cb)->
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)->
@ -137,6 +140,9 @@ module.exports = ProjectController =
return next(err)
logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0]
notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
user = results.user
ProjectController._injectProjectOwners projects, (error, projects) ->
@ -147,6 +153,7 @@ module.exports = ProjectController =
priority_title: true
projects: projects
tags: tags
notifications: notifications or []
user: user
hasSubscription: results.hasSubscription[0]
}
@ -205,6 +212,7 @@ module.exports = ProjectController =
user = results.user
subscription = results.subscription
daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000
logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor"
@ -214,6 +222,7 @@ module.exports = ProjectController =
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
allowedFreeTrial = !!subscription.freeTrial.allowed || true
logger.log project_id:project_id, "rendering editor page"
res.render 'project/editor',
title: project.name
@ -311,4 +320,3 @@ do generateThemeList = () ->
if file.slice(-2) == "js" and file.match(/^theme-/)
cleanName = file.slice(0,-3).slice(6)
THEME_LIST.push cleanName

View file

@ -19,6 +19,8 @@ module.exports =
project = new Project
owner_ref : new ObjectId(owner_id)
name : projectName
if Settings.currentImageName?
project.imageName = Settings.currentImageName
project.rootFolder[0] = rootFolder
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
project.spellCheckLanguage = user.ace.spellCheckLanguage
@ -33,7 +35,9 @@ module.exports =
self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)->
return callback(error) if error?
ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)->
return callback(error) if error?
if error?
logger.err err:error, "error adding doc when creating basic project"
return callback(error)
ProjectEntityHandler.setRootDoc project._id, doc._id, (error) ->
callback(error, project)

View file

@ -1,4 +1,5 @@
Project = require('../../models/Project').Project
ProjectGetter = require("./ProjectGetter")
logger = require('logger-sharelatex')
documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
tagsHandler = require("../Tags/TagsHandler")
@ -33,10 +34,10 @@ module.exports = ProjectDeleter =
Project.remove _id: project_id, callback
archiveProject: (project_id, callback = (error) ->)->
logger.log project_id:project_id, "deleting project"
Project.findById project_id, (err, project)=>
logger.log project_id:project_id, "archived project from user request"
ProjectGetter.getProject project_id, {owner_ref:true, collaberator_refs:true, readOnly_refs:true}, (err, project)=>
if err? or !project?
logger.err err:err, project_id:project_id, "error getting project to delete it"
logger.err err:err, project_id:project_id, "error getting project to archived it"
callback(err)
else
async.series [
@ -57,8 +58,10 @@ module.exports = ProjectDeleter =
Project.update {_id:project_id}, { $set: { archived: true }}, cb
], (err)->
if err?
logger.err err:err, "problem deleting project"
callback(err)
logger.err err:err, "problem archived project"
return callback(err)
logger.log project_id:project_id, "succesfully archived project from user request"
callback()
restoreProject: (project_id, callback = (error) ->) ->
Project.update {_id:project_id}, { $unset: { archived: true }}, callback

View file

@ -8,7 +8,7 @@ _ = require("underscore")
module.exports =
getDetails: (project_id, callback)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true}, (err, project)->
if err?
logger.err err:err, project_id:project_id, "error getting project"
return callback(err)
@ -37,7 +37,7 @@ module.exports =
renameProject: (project_id, newName, callback = ->)->
logger.log project_id: project_id, newName:newName, "renaming project"
ProjectGetter.getProject project_id, {"name":1}, (err, project)->
ProjectGetter.getProject project_id, {name:true}, (err, project)->
if err? or !project?
logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
return callback(err)

View file

@ -4,61 +4,86 @@ projectLocator = require('./ProjectLocator')
projectOptionsHandler = require('./ProjectOptionsHandler')
DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler")
DocstoreManager = require "../Docstore/DocstoreManager"
Project = require("../../models/Project").Project
ProjectGetter = require("./ProjectGetter")
_ = require('underscore')
async = require('async')
logger = require("logger-sharelatex")
module.exports =
duplicate: (owner, originalProjectId, newProjectName, callback)->
DocumentUpdaterHandler.flushProjectToMongo originalProjectId, (err) ->
return callback(err) if err?
Project.findById originalProjectId, (err, originalProject) ->
return callback(err) if err?
projectCreationHandler.createBlankProject owner._id, newProjectName, (err, newProject)->
return callback(err) if err?
projectLocator.findRootDoc {project:originalProject}, (err, originalRootDoc)->
return callback(err) if err?
DocstoreManager.getAllDocs originalProjectId, (err, docContentsArray) ->
return callback(err) if err?
docContents = {}
for docContent in docContentsArray
docContents[docContent._id] = docContent
module.exports = ProjectDuplicator =
projectOptionsHandler.setCompiler newProject._id, originalProject.compiler
_copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
setRootDoc = _.once (doc_id)->
projectEntityHandler.setRootDoc newProject._id, doc_id
setRootDoc = _.once (doc_id)->
projectEntityHandler.setRootDoc newProject, doc_id
jobs = originalFolder.docs.map (doc)->
return (cb)->
content = docContents[doc._id.toString()]
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)->
if err?
logger.err err:err, "error copying doc"
return callback(err)
if originalRootDoc? and newDoc.name == originalRootDoc.name
setRootDoc newDoc._id
cb()
copyDocs = (originalFolder, newParentFolder, callback)->
jobs = originalFolder.docs.map (doc)->
return (callback)->
content = docContents[doc._id.toString()]
return callback(new Error("doc_id not found: #{doc._id}")) if !content?
projectEntityHandler.addDoc newProject, newParentFolder._id, doc.name, content.lines, (err, newDoc)->
if originalRootDoc? and newDoc.name == originalRootDoc.name
setRootDoc newDoc._id
callback()
async.series jobs, callback
async.series jobs, callback
copyFiles = (originalFolder, newParentFolder, callback)->
jobs = originalFolder.fileRefs.map (file)->
return (callback)->
projectEntityHandler.copyFileFromExistingProject newProject, newParentFolder._id, originalProject._id, file, callback
async.parallelLimit jobs, 5, callback
_copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
jobs = originalFolder.fileRefs.map (file)->
return (cb)->
projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
async.parallelLimit jobs, 5, callback
copyFolder = (folder, desFolder, callback)->
jobs = folder.folders.map (childFolder)->
return (callback)->
projectEntityHandler.addFolder newProject, desFolder._id, childFolder.name, (err, newFolder)->
copyFolder childFolder, newFolder, callback
jobs.push (cb)->
copyDocs folder, desFolder, cb
jobs.push (cb)->
copyFiles folder, desFolder, cb
async.series jobs, callback
_copyFolderRecursivly: (newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)->
ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)->
if err?
logger.err project_id:newProject_id, "could not get project"
return cb(err)
copyFolder originalProject.rootFolder[0], newProject.rootFolder[0], ->
callback(err, newProject)
jobs = originalFolder.folders.map (childFolder)->
return (cb)->
projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)->
return cb(err) if err?
ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb
jobs.push (cb)->
ProjectDuplicator._copyFiles newProject, originalProject_id, originalFolder, desFolder, cb
jobs.push (cb)->
ProjectDuplicator._copyDocs newProject, originalRootDoc, originalFolder, desFolder, docContents, cb
async.series jobs, callback
duplicate: (owner, originalProject_id, newProjectName, callback)->
jobs =
flush: (cb)->
DocumentUpdaterHandler.flushProjectToMongo originalProject_id, cb
originalProject: (cb)->
ProjectGetter.getProject originalProject_id, {compiler:true, rootFolder:true, rootDoc_id:true}, cb
newProject: (cb)->
projectCreationHandler.createBlankProject owner._id, newProjectName, cb
originalRootDoc: (cb)->
projectLocator.findRootDoc {project_id:originalProject_id}, cb
docContentsArray: (cb)->
DocstoreManager.getAllDocs originalProject_id, cb
async.series jobs, (err, results)->
if err?
logger.err err:err, originalProject_id:originalProject_id, "error duplicating project"
return callback(err)
{originalProject, newProject, originalRootDoc, docContentsArray} = results
originalRootDoc = originalRootDoc[0]
docContents = {}
for docContent in docContentsArray
docContents[docContent._id] = docContent
projectOptionsHandler.setCompiler newProject._id, originalProject.compiler, ->
ProjectDuplicator._copyFolderRecursivly newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, ->
if err?
logger.err err:err, originalProject_id:originalProject_id, newProjectName:newProjectName, "error cloning project"
callback(err, newProject)

View file

@ -26,6 +26,8 @@ module.exports = ProjectEditorHandler =
dropbox:false
compileTimeout: 60
compileGroup:"standard"
templates: false
references: false
if project.owner_ref.features?
if project.owner_ref.features.collaborators?
@ -38,6 +40,10 @@ module.exports = ProjectEditorHandler =
result.features.compileTimeout = project.owner_ref.features.compileTimeout
if project.owner_ref.features.compileGroup?
result.features.compileGroup = project.owner_ref.features.compileGroup
if project.owner_ref.features.templates?
result.features.templates = project.owner_ref.features.templates
if project.owner_ref.features.references?
result.features.references = project.owner_ref.features.references
result.owner = @buildUserModelView project.owner_ref, "owner"

View file

@ -1,4 +1,5 @@
Project = require('../../models/Project').Project
settings = require "settings-sharelatex"
Doc = require('../../models/Doc').Doc
Folder = require('../../models/Folder').Folder
File = require('../../models/File').File
@ -75,23 +76,21 @@ module.exports = ProjectEntityHandler =
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
documentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if error?
Project.findById project_id, (error, project) ->
ProjectGetter.getProject project_id, {name:true}, (error, project) ->
return callback(error) if error?
requests = []
self.getAllDocs project_id, (error, docs) ->
return callback(error) if error?
for docPath, doc of docs
do (docPath, doc) ->
requests.push (callback) ->
tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0},
callback
requests.push (cb) ->
tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb
self.getAllFiles project_id, (error, files) ->
return callback(error) if error?
for filePath, file of files
do (filePath, file) ->
requests.push (callback) ->
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev},
callback
requests.push (cb) ->
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb
async.series requests, (err) ->
logger.log project_id:project_id, "finished flushing project to tpds"
callback(err)
@ -110,27 +109,36 @@ module.exports = ProjectEntityHandler =
options = {}
DocstoreManager.getDoc project_id, doc_id, options, callback
addDoc: (project_or_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
Project.getProject project_or_id, "", (err, project) ->
logger.log project: project._id, folder_id: folder_id, doc_name: docName, "adding doc"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)=>
doc = new Doc name: docName
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
# which hasn't been created in docstore.
DocstoreManager.updateDoc project._id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
addDoc: (project_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add doc"
return callback(err)
ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, callback
addDocWithProject: (project, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
project_id = project._id
logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
confirmFolder project, folder_id, (folder_id)=>
doc = new Doc name: docName
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
# which hasn't been created in docstore.
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
return callback(err) if err?
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
return callback(err) if err?
Project.putElement project._id, folder_id, doc, "doc", (err, result)=>
return callback(err) if err?
tpdsUpdateSender.addDoc {
project_id: project._id,
doc_id: doc._id
path: result.path.fileSystem,
project_name: project.name,
rev: 0
}, (err) ->
return callback(err) if err?
callback(null, doc, folder_id)
tpdsUpdateSender.addDoc {
project_id: project_id,
doc_id: doc?._id
path: result?.path?.fileSystem,
project_name: project.name,
rev: 0
}, (err) ->
if err?
logger.err err:err, "error adding doc to tpdsworker, contining anyway"
callback(null, doc, folder_id)
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
# getDoc will return the deleted doc's lines, but we don't actually remove
@ -139,22 +147,34 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
ProjectEntityHandler.addDoc project_id, null, name, lines, callback
addFile: (project_or_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
ProjectGetter.getProjectWithOnlyFolders project_or_id, (err, project) ->
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, project_name:project.name, rev:fileRef.rev}, ->
callback(err, fileRef, folder_id)
addFile: (project_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for add file"
return callback(err)
ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, callback
replaceFile: (project_or_id, file_id, fsPath, callback)->
Project.getProject project_or_id, "", (err, project) ->
addFileWithProject: (project, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)->
fileRef = new File name : fileName
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
return callback(err)
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err)->
if err?
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error sending file to tpdsworker"
callback(null, fileRef, folder_id)
replaceFile: (project_id, file_id, fsPath, callback)->
ProjectGetter.getProject project_id, {name:true}, (err, project) ->
return callback(err) if err?
findOpts =
project_id:project._id
@ -182,21 +202,36 @@ module.exports = ProjectEntityHandler =
Project.update conditons, update, {}, (err, second)->
callback()
copyFileFromExistingProject: (project_or_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
Project.getProject project_or_id, "", (err, project) ->
logger.log project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)=>
if !origonalFileRef?
logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null"
return callback()
fileRef = new File name : origonalFileRef.name
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
copyFileFromExistingProject: (project_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
ProjectGetter.getProject project_id, {name:true}, (err, project) ->
if err?
logger.err project_id:project_id, err:err, "error getting project for copy file from existing project"
return callback(err)
ProjectEntityHandler.copyFileFromExistingProjectWithProject project, folder_id, originalProject_id, origonalFileRef, callback
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3 with project"
return callback(err) if err?
confirmFolder project, folder_id, (folder_id)=>
if !origonalFileRef?
logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null"
return callback()
fileRef = new File name : origonalFileRef.name
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
if err?
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
return callback(err)
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err?
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, (error) ->
callback(error, fileRef, folder_id)
logger.err err:err, project_id:project._id, folder_id:folder_id, "error putting element as part of copy"
return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
if err?
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error sending file to tpds worker"
callback(null, fileRef, folder_id)
mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
self = @
@ -204,7 +239,7 @@ module.exports = ProjectEntityHandler =
folders = _.select folders, (folder)->
return folder.length != 0
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)=>
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
if path == '/'
logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder"
return callback(null, [], project.rootFolder[0])
@ -217,7 +252,7 @@ module.exports = ProjectEntityHandler =
if parentFolder?
parentFolder_id = parentFolder._id
builtUpPath = "#{builtUpPath}/#{folderName}"
projectLocator.findElementByPath project_id, builtUpPath, (err, foundFolder)=>
projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=>
if !foundFolder?
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
@ -236,15 +271,22 @@ module.exports = ProjectEntityHandler =
!folder.filterOut
callback(null, folders, lastFolder)
addFolder: (project_or_id, parentFolder_id, folderName, callback) ->
folder = new Folder name: folderName
Project.getProject project_or_id, "", (err, project) ->
return callback(err) if err?
confirmFolder project, parentFolder_id, (parentFolder_id)=>
logger.log project: project_or_id, parentFolder_id:parentFolder_id, folderName:folderName, "new folder added"
Project.putElement project._id, parentFolder_id, folder, "folder", (err, result)=>
if callback?
callback(err, folder, parentFolder_id)
addFolder: (project_id, parentFolder_id, folderName, callback) ->
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
if err?
logger.err project_id:project_id, err:err, "error getting project for add folder"
return callback(err)
ProjectEntityHandler.addFolderWithProject project, parentFolder_id, folderName, callback
addFolderWithProject: (project, parentFolder_id, folderName, callback = (err, folder, parentFolder_id)->) ->
confirmFolder project, parentFolder_id, (parentFolder_id)=>
folder = new Folder name: folderName
logger.log project: project._id, parentFolder_id:parentFolder_id, folderName:folderName, "adding new folder"
ProjectEntityHandler._putElement project, parentFolder_id, folder, "folder", (err, result)=>
if err?
logger.err err:err, project_id:project._id, "error adding folder to project"
return callback(err)
callback(err, folder, parentFolder_id)
updateDocLines : (project_id, doc_id, lines, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
@ -281,7 +323,7 @@ module.exports = ProjectEntityHandler =
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
return callback(err) if err?
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)->
return callback(err) if err?
@ -302,7 +344,7 @@ module.exports = ProjectEntityHandler =
return callback(error) if error?
self._removeElementFromMongoArray Project, project_id, path.mongo, (err)->
return callback(err) if err?
Project.putElement project_id, destinationFolder_id, entity, entityType, (err, result)->
ProjectEntityHandler._putElement project, destinationFolder_id, entity, entityType, (err, result)->
return callback(err) if err?
opts =
project_id:project_id
@ -319,7 +361,7 @@ module.exports = ProjectEntityHandler =
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (err, project)=>
return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
return callback(error) if error?
@ -338,7 +380,7 @@ module.exports = ProjectEntityHandler =
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set")
entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=>
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=>
if err?
return callback err
@ -426,6 +468,72 @@ module.exports = ProjectEntityHandler =
}
}, {}, callback
_countElements : (project, callback)->
countFolder = (folder, cb = (err, count)->)->
jobs = _.map folder?.folders, (folder)->
(asyncCb)-> countFolder folder, asyncCb
async.series jobs, (err, subfolderCounts)->
total = 0
if subfolderCounts?.length > 0
total = _.reduce subfolderCounts, (a, b)-> return a + b
if folder?.folders?.length?
total += folder?.folders?.length
if folder?.docs?.length?
total += folder?.docs?.length
if folder?.fileRefs?.length?
total += folder?.fileRefs?.length
cb(null, total)
countFolder project.rootFolder[0], callback
_putElement: (project, folder_id, element, type, callback = (err, path)->)->
sanitizeTypeOfElement = (elementType)->
lastChar = elementType.slice -1
if lastChar != "s"
elementType +="s"
if elementType == "files"
elementType = "fileRefs"
return elementType
if !element?
e = new Error("no element passed to be inserted")
logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null"
return callback(e)
type = sanitizeTypeOfElement type
if !folder_id?
folder_id = project.rootFolder[0]._id
ProjectEntityHandler._countElements project, (err, count)->
if count > settings.maxEntitiesPerProject
logger.warn project_id:project._id, "project too big, stopping insertions"
return callback("project_has_to_many_files")
projectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=>
if err?
logger.err err:err, project_id:project._id, folder_id:folder_id, type:type, element:element, "error finding folder for _putElement"
return callback(err)
newPath =
fileSystem: "#{path.fileSystem}/#{element.name}"
mongo: path.mongo
logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, "adding element to project"
id = element._id+''
element._id = require('mongoose').Types.ObjectId(id)
conditions = _id:project._id
mongopath = "#{path.mongo}.#{type}"
update = "$push":{}
update["$push"][mongopath] = element
Project.update conditions, update, {}, (err)->
if err?
logger.err err: err, project_id: project._id, 'error saving in putElement project'
return callback(err)
callback(err, {path:newPath})
confirmFolder = (project, folder_id, callback)->
logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project"
if folder_id+'' == 'undefined'

View file

@ -2,31 +2,54 @@ mongojs = require("../../infrastructure/mongojs")
db = mongojs.db
ObjectId = mongojs.ObjectId
async = require "async"
Errors = require("../../errors")
logger = require("logger-sharelatex")
module.exports = ProjectGetter =
EXCLUDE_DEPTH: 8
getProjectWithoutDocLines: (project_id, callback=(error, project) ->) ->
excludes = {}
for i in [1..@EXCLUDE_DEPTH]
for i in [1..ProjectGetter.EXCLUDE_DEPTH]
excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
callback error, projects[0]
getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) ->
excludes = {}
for i in [1..@EXCLUDE_DEPTH]
for i in [1..ProjectGetter.EXCLUDE_DEPTH]
excludes["rootFolder#{Array(i).join(".folder")}.docs"] = 0
excludes["rootFolder#{Array(i).join(".folder")}.fileRefs"] = 0
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
callback error, projects[0]
getProject: (query, projection, callback = (error, project) ->) ->
if !query?
return callback("no query provided")
if typeof(projection) == "function"
callback = projection
if typeof query == "string"
query = _id: ObjectId(query)
else if query instanceof ObjectId
query = _id: query
db.projects.findOne query, projection, callback
else if query?.toString().length == 24 # sometimes mongoose ids are hard to identify, this will catch them
query = _id: ObjectId(query.toString())
else
err = new Error("malformed get request")
logger.log query:query, err:err, type:typeof(query), "malformed get request"
return callback(err)
db.projects.find query, projection, (err, project)->
if err?
logger.err err:err, query:query, projection:projection, "error getting project"
return callback(err)
callback(null, project?[0])
populateProjectWithUsers: (project, callback=(error, project) ->) ->
# eventually this should be in a UserGetter.getUser module

View file

@ -1,11 +1,16 @@
Project = require('../../models/Project').Project
ProjectGetter = require("./ProjectGetter")
Errors = require "../../errors"
_ = require('underscore')
logger = require('logger-sharelatex')
async = require('async')
module.exports =
findElement: (options, callback = (err, element, path, parentFolder)->)->
module.exports = ProjectLocator =
findElement: (options, _callback = (err, element, path, parentFolder)->)->
callback = (args...) ->
_callback(args...)
_callback = () ->
{project, project_id, element_id, type} = options
elementType = sanitizeTypeOfElement type
@ -46,7 +51,7 @@ module.exports =
if project?
startSearch(project)
else
Project.findById project_id, (err, project)->
ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
return callback(err) if err?
if !project?
return callback(new Errors.NotFoundError("project not found"))
@ -62,8 +67,12 @@ module.exports =
if project?
getRootDoc project
else
Project.findById project_id, (err, project)->
getRootDoc project
ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
if err?
logger.err err:err, "error getting project"
return callback(err)
else
getRootDoc project
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
@ -122,11 +131,11 @@ module.exports =
async.waterfall jobs, callback
findUsersProjectByName: (user_id, projectName, callback)->
Project.findAllUsersProjects user_id, 'name', (err, projects, collabertions=[])->
Project.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])->
projects = projects.concat(collabertions)
projectName = projectName.toLowerCase()
project = _.find projects, (project)->
project.name.toLowerCase() == projectName
project.name.toLowerCase() == projectName and project.archived != true
logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name"
callback(null, project)

View file

@ -0,0 +1,37 @@
logger = require('logger-sharelatex')
ReferencesHandler = require('./ReferencesHandler')
settings = require('settings-sharelatex')
EditorRealTimeController = require("../Editor/EditorRealTimeController")
module.exports = ReferencesController =
index: (req, res) ->
projectId = req.params.Project_id
shouldBroadcast = req.body.shouldBroadcast
docIds = req.body.docIds
if (!docIds or !(docIds instanceof Array))
logger.err {projectId, docIds}, "docIds is not valid, should be either Array or String 'ALL'"
return res.sendStatus 400
logger.log {projectId, docIds: docIds}, "index references for project"
ReferencesHandler.index projectId, docIds, (err, data) ->
if err
logger.err {err, projectId}, "error indexing all references"
return res.sendStatus 500
ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
indexAll: (req, res) ->
projectId = req.params.Project_id
shouldBroadcast = req.body.shouldBroadcast
logger.log {projectId}, "index all references for project"
ReferencesHandler.indexAll projectId, (err, data) ->
if err
logger.err {err, projectId}, "error indexing all references"
return res.sendStatus 500
ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
_handleIndexResponse: (req, res, projectId, shouldBroadcast, data) ->
if shouldBroadcast
logger.log {projectId}, "emitting new references keys to connected clients"
EditorRealTimeController.emitToRoom projectId, 'references:keys:updated', data.keys
return res.json data

View file

@ -0,0 +1,85 @@
logger = require("logger-sharelatex")
request = require("request")
settings = require("settings-sharelatex")
Project = require("../../models/Project").Project
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
U = require('underscore')
Async = require('async')
oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5
module.exports = ReferencesHandler =
_buildDocUrl: (projectId, docId) ->
"#{settings.apis.docstore.url}/project/#{projectId}/doc/#{docId}/raw"
_findBibDocIds: (project) ->
ids = []
_process = (folder) ->
folder.docs.forEach (doc) ->
if doc?.name?.match(/^.*\.bib$/)
ids.push(doc._id)
folder.folders.forEach (folder) ->
_process(folder)
project.rootFolder.forEach (rootFolder) ->
_process(rootFolder)
return ids
_isFullIndex: (project, callback = (err, result) ->) ->
owner = project.owner_ref
callback(null, owner.features.references == true)
indexAll: (projectId, callback=(err, data)->) ->
Project.findPopulatedById projectId, (err, project) ->
if err
logger.err {err, projectId}, "error finding project"
return callback(err)
logger.log {projectId}, "indexing all bib files in project"
docIds = ReferencesHandler._findBibDocIds(project)
ReferencesHandler._doIndexOperation(projectId, project, docIds, callback)
index: (projectId, docIds, callback=(err, data)->) ->
Project.findPopulatedById projectId, (err, project) ->
if err
logger.err {err, projectId}, "error finding project"
return callback(err)
ReferencesHandler._doIndexOperation(projectId, project, docIds, callback)
_doIndexOperation: (projectId, project, docIds, callback) ->
ReferencesHandler._isFullIndex project, (err, isFullIndex) ->
if err
logger.err {err, projectId}, "error checking whether to do full index"
return callback(err)
logger.log {projectId, docIds}, 'flushing docs to mongo before calling references service'
Async.series(
docIds.map((docId) -> (cb) -> DocumentUpdaterHandler.flushDocToMongo(projectId, docId, cb)),
(err) ->
# continue
if err
logger.err {err, projectId, docIds}, "error flushing docs to mongo"
return callback(err)
bibDocUrls = docIds.map (docId) ->
ReferencesHandler._buildDocUrl projectId, docId
logger.log {projectId, isFullIndex, docIds, bibDocUrls}, "sending request to references service"
request.post {
url: "#{settings.apis.references.url}/project/#{projectId}/index"
json:
docUrls: bibDocUrls
fullIndex: isFullIndex
}, (err, res, data) ->
if err
logger.err {err, projectId}, "error communicating with references api"
return callback(err)
if 200 <= res.statusCode < 300
logger.log {projectId}, "got keys from references api"
return callback(null, data)
else
err = new Error("references api responded with non-success code: #{res.statusCode}")
logger.log {err, projectId}, "error updating references"
return callback(err)
)

View file

@ -15,7 +15,7 @@ module.exports = RateLimiterMiddlewear =
###
rateLimit: (opts) ->
return (req, res, next) ->
if req.session.user?
if req.session?.user?
user_id = req.session.user._id
else
user_id = req.ip

View file

@ -12,5 +12,4 @@ module.exports = SpellingController =
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
.on "error", (error) ->
logger.error err: error, "Spelling API error"
res.sendStatus 500
.pipe(res)

View file

@ -13,6 +13,7 @@ module.exports =
webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs")
webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")

View file

@ -52,11 +52,17 @@ module.exports =
return callback(err) if err?
callback err, subscriptions.length > 0, subscriptions
hasGroupMembersLimitReached: (user_id, callback)->
hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)->
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if err?
logger.err err:err, user_id:user_id, "error getting users subscription"
return callback(err)
if !subscription?
logger.err user_id:user_id, "no subscription found for user"
return callback("no subscription found")
limitReached = subscription.member_ids.length >= subscription.membersLimit
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
callback(err, limitReached)
callback(err, limitReached, subscription)
getOwnerOfProject = (project_id, callback)->
Project.findById project_id, 'owner_ref', (error, project) ->

View file

@ -247,6 +247,18 @@ module.exports = RecurlyWrapper =
callback(error)
)
extendTrial: (subscriptionId, daysUntilExpire = 7, callback)->
next_renewal_date = new Date()
next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
@apiRequest({
url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
method : "put"
}, (error, response, responseBody) =>
if error?
logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
callback(error)
)
_parseSubscriptionXml: (xml, callback) ->
@_parseXml xml, (error, data) ->

View file

@ -1,7 +1,6 @@
SecurityManager = require '../../managers/SecurityManager'
SubscriptionHandler = require './SubscriptionHandler'
PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
LimitationsManager = require("./LimitationsManager")
RecurlyWrapper = require './RecurlyWrapper'
@ -9,7 +8,6 @@ Settings = require 'settings-sharelatex'
logger = require('logger-sharelatex')
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
require("../../infrastructure/Sixpack")
module.exports = SubscriptionController =
@ -98,14 +96,14 @@ module.exports = SubscriptionController =
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groups) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, "showing subscription dashboard"
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groups:groups, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription
subscription: subscription || {}
groups: groups
subscriptionTabActive: true
@ -226,6 +224,14 @@ module.exports = SubscriptionController =
else
res.sendStatus 200
extendTrial: (req, res)->
SecurityManager.getCurrentUser req, (error, user) ->
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500
else
res.send 200
recurlyNotificationParser: (req, res, next) ->
xml = ""

View file

@ -1,30 +1,17 @@
async = require("async")
_ = require("underscore")
settings = require("settings-sharelatex")
SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
_s = require("underscore.string")
module.exports = SubscriptionDomainHandler =
getLicenceUserCanJoin: (user, callback)->
getLicenceUserCanJoin: (user)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
if licence?
callback null, licence
else
callback()
attemptToJoinGroup: (user, callback)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
if licence? and user.emailVerified
SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, callback
else
callback "user not verified"
return licence
rejectInvitationToGroup: (user, subscription, callback)->
removeUserFromGroup(subscription.admin_id, user._id, callback)
getDomainLicencePage: (user)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
if licence?.verifyEmail
@ -32,20 +19,11 @@ module.exports = SubscriptionDomainHandler =
else
return undefined
autoAllocate: (user, callback = ->)->
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
#
if licence?
SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, callback
else
callback()
_findDomainLicence: (email)->
licence = _.find settings.domainLicences, (licence)->
_.find licence.domains, (domain)->
_s.endsWith email, domain
regex = "[@\.]#{domain}"
return email.match(regex)
return licence
@ -53,4 +31,3 @@ module.exports = SubscriptionDomainHandler =
licence = _.find settings.domainLicences, (licence)->
licence?.subscription_id == subscription_id
return licence

View file

@ -1,10 +1,7 @@
SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator")
ErrorsController = require("../Errors/ErrorController")
settings = require("settings-sharelatex")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
_ = require("underscore")
@ -12,9 +9,12 @@ module.exports =
addUserToGroup: (req, res)->
adminUserId = req.session.user._id
newEmail = req.body.email
newEmail = req.body?.email?.toLowerCase()?.trim()
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
if err?
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
return res.sendStatus 500
result =
user:user
if err and err.limitReached
@ -25,7 +25,20 @@ module.exports =
adminUserId = req.session.user._id
userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, ->
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
if err?
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
return res.sendStatus 500
res.send()
removeSelfFromGroup: (req, res)->
adminUserId = req.query.admin_user_id
userToRemove_id = req.session.user._id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
if err?
logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group"
return res.sendStatus 500
res.send()
renderSubscriptionGroupAdminPage: (req, res)->
@ -70,13 +83,16 @@ module.exports =
subscription_id = req.params.subscription_id
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
return ErrorsController.notFound(req, res)
SubscriptionGroupHandler.processGroupVerification req.session.user.email, subscription_id, req.query.token, (err)->
email = req?.session?.user?.email
logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group"
SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)->
if err? and err == "token_not_found"
res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
else if err?
res.sendStatus 500
return res.sendStatus 500
else
res.redirect "/user/subscription/#{subscription_id}/group/successful-join"
logger.log subscription_id:subscription_id, email:email, "user successful completed join of group subscription"
return res.redirect "/user/subscription/#{subscription_id}/group/successful-join"
renderSuccessfulJoinPage: (req, res)->
subscription_id = req.params.subscription_id

View file

@ -9,15 +9,32 @@ logger = require("logger-sharelatex")
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
EmailHandler = require("../Email/EmailHandler")
settings = require("settings-sharelatex")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = SubscriptionGroupHandler =
addUserToGroup: (adminUser_id, newEmail, callback)->
addUserToGroup: (adminUserId, newEmail, callback)->
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group"
UserCreator.getUserOrCreateHoldingAccount newEmail, (err, user)->
LimitationsManager.hasGroupMembersLimitReached adminUser_id, (err, limitReached)->
if err?
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error creating user for holding account"
return callback(err)
if !user?
msg = "no user returned whenc reating holidng account or getting user"
logger.err adminUserId:adminUserId, newEmail:newEmail, msg
return callback(msg)
LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)->
if err?
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan"
return callback(err)
if limitReached
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
return callback(limitReached:limitReached)
SubscriptionUpdater.addUserToGroup adminUser_id, user._id, (err)->
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
if err?
logger.err err:err, "error adding user to group"
return callback(err)
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
userViewModel = buildUserViewModel(user)
callback(err, userViewModel)
@ -60,13 +77,20 @@ module.exports = SubscriptionGroupHandler =
EmailHandler.sendEmail "completeJoinGroupAccount", opts, callback
processGroupVerification: (userEmail, subscription_id, token, callback)->
logger.log userEmail:userEmail, subscription_id:subscription_id, "processing group verification for user"
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, token_subscription_id)->
if err? or subscription_id != token_subscription_id
logger.err userEmail:userEmail, token:token, "token value not found for processing group verification"
return callback("token_not_found")
SubscriptionLocator.getSubscription subscription_id, (err, subscription)->
SubscriptionGroupHandler.addUserToGroup subscription.admin_id, userEmail, callback
if err?
logger.err err:err, subscription:subscription, userEmail:userEmail, subscription_id:subscription_id, "error getting subscription"
return callback(err)
if !subscription?
logger.warn subscription_id:subscription_id, userEmail:userEmail, "no subscription found"
return callback()
SubscriptionGroupHandler.addUserToGroup subscription?.admin_id, userEmail, callback
buildUserViewModel = (user)->

View file

@ -74,5 +74,5 @@ module.exports =
return callback("no user found")
SubscriptionUpdater.syncSubscription recurlySubscription, user?._id, callback
extendTrial: (subscription, daysToExend, callback)->
RecurlyWrapper.extendTrial subscription.recurlySubscription_id, daysToExend, callback

View file

@ -22,4 +22,4 @@ module.exports =
Subscription.findOne _id:subscription_id, callback
getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)->
Subscription.findOne member_ids: user_id, _id:subscription_id, callback
Subscription.findOne member_ids: user_id, _id:subscription_id, {_id:1}, callback

View file

@ -24,6 +24,8 @@ module.exports =
webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup
webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage
webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup
@ -39,6 +41,7 @@ module.exports =
webRouter.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription
webRouter.put '/user/subscription/extend', AuthenticationController.requireLogin(), SubscriptionController.extendTrial
webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan

View file

@ -32,6 +32,9 @@ module.exports =
insertOperation =
"$addToSet": {member_ids:user_id}
Subscription.findAndModify searchOps, insertOperation, (err, subscription)->
if err?
logger.err err:err, searchOps:searchOps, insertOperation:insertOperation, "error findy and modify add user to group"
return callback(err)
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
removeUserFromGroup: (adminUser_id, user_id, callback)->
@ -39,10 +42,12 @@ module.exports =
admin_id: adminUser_id
removeOperation =
"$pull": {member_ids:user_id}
Subscription.update searchOps, removeOperation, ->
Subscription.update searchOps, removeOperation, (err)->
if err?
logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group"
return callback(err)
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback
_createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription"
subscription = new Subscription(admin_id:adminUser_id)

View file

@ -27,6 +27,7 @@ module.exports =
currency:recurlySubscription?.currency
taxRate:parseFloat(recurlySubscription?.tax_rate?._)
groupPlan: subscription.groupPlan
trial_ends_at:recurlySubscription?.trial_ends_at
}, memberSubscriptions
else
callback null, null, memberSubscriptions

View file

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

View file

@ -2,20 +2,53 @@ TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex")
module.exports =
processTagsUpdate: (req, res)->
getAllTags: (req, res, next)->
user_id = req.session.user._id
project_id = req.params.project_id
if req.body.deletedTag?
tag = req.body.deletedTag
TagsHandler.deleteTag user_id, project_id, tag, ->
res.send()
else
tag = req.body.tag
TagsHandler.addTag user_id, project_id, tag, ->
res.send()
logger.log user_id:user_id, project_id:project_id, body:req.body, "processing tag update"
logger.log {user_id}, "getting tags"
TagsHandler.getAllTags user_id, (error, allTags)->
return next(error) if error?
res.json(allTags)
getAllTags: (req, res)->
TagsHandler.getAllTags req.session.user._id, (err, allTags)->
res.send(allTags)
createTag: (req, res, next) ->
user_id = req.session.user._id
name = req.body.name
logger.log {user_id, name}, "creating tag"
TagsHandler.createTag user_id, name, (error, tag) ->
return next(error) if error?
res.json(tag)
addProjectToTag: (req, res, next) ->
user_id = req.session.user._id
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "adding tag to project"
TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
removeProjectFromTag: (req, res, next) ->
user_id = req.session.user._id
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "removing tag from project"
TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
deleteTag: (req, res, next) ->
user_id = req.session.user._id
tag_id = req.params.tag_id
logger.log {user_id, tag_id}, "deleting tag"
TagsHandler.deleteTag user_id, tag_id, (error) ->
return next(error) if error?
res.status(204).end()
renameTag: (req, res, next) ->
user_id = req.session.user._id
tag_id = req.params.tag_id
name = req.body?.name
if !name?
return res.status(400).end()
else
logger.log {user_id, tag_id, name}, "renaming tag"
TagsHandler.renameTag user_id, tag_id, name, (error) ->
return next(error) if error?
res.status(204).end()

View file

@ -3,61 +3,84 @@ settings = require("settings-sharelatex")
request = require("request")
logger = require("logger-sharelatex")
oneSecond = 1000
module.exports =
deleteTag: (user_id, project_id, tag, callback)->
uri = buildUri(user_id, project_id)
opts =
uri:uri
json:
name:tag
timeout:oneSecond
logger.log user_id:user_id, project_id:project_id, tag:tag, "send delete tag to tags api"
request.del opts, callback
addTag: (user_id, project_id, tag, callback)->
uri = buildUri(user_id, project_id)
opts =
uri:uri
json:
name:tag
timeout:oneSecond
logger.log user_id:user_id, project_id:project_id, tag:tag, "send add tag to tags api"
request.post opts, callback
requestTags: (user_id, callback)->
opts =
uri: "#{settings.apis.tags.url}/user/#{user_id}/tag"
json: true
timeout: 2000
request.get opts, (err, res, body)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting tags, #{err}, #{statusCode}")
logger.err err:err
callback(e, [])
else
callback(null, body)
TIMEOUT = 1000
module.exports = TagsHandler =
getAllTags: (user_id, callback)->
@requestTags user_id, (err, allTags)=>
@_requestTags user_id, (err, allTags)=>
if !allTags?
allTags = []
@groupTagsByProject allTags, (err, groupedByProject)->
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "getting all tags from tags api"
@_groupTagsByProject allTags, (err, groupedByProject)->
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "got all tags from tags api"
callback err, allTags, groupedByProject
removeProjectFromAllTags: (user_id, project_id, callback)->
uri = buildUri(user_id, project_id)
createTag: (user_id, name, callback = (error, tag) ->) ->
opts =
uri:"#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
timeout:oneSecond
logger.log user_id:user_id, project_id:project_id, "removing project_id from tags"
request.del opts, callback
url: "#{settings.apis.tags.url}/user/#{user_id}/tag"
json:
name: name
timeout: TIMEOUT
request.post opts, (err, res, body)->
TagsHandler._handleResponse err, res, {user_id}, (error) ->
return callback(error) if error?
callback(null, body or {})
groupTagsByProject: (tags, callback)->
renameTag: (user_id, tag_id, name, callback = (error) ->) ->
url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/rename"
request.post {
url: url
json:
name: name
timeout: TIMEOUT
}, (err, res, body) ->
TagsHandler._handleResponse err, res, {url, user_id, tag_id, name}, callback
deleteTag: (user_id, tag_id, callback = (error) ->) ->
url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}"
request.del {url, timeout: TIMEOUT}, (err, res, body) ->
TagsHandler._handleResponse err, res, {url, user_id, tag_id}, callback
removeProjectFromTag: (user_id, tag_id, project_id, callback)->
url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}"
request.del {url, timeout: TIMEOUT}, (err, res, body) ->
TagsHandler._handleResponse err, res, {url, user_id, tag_id, project_id}, callback
addProjectToTag: (user_id, tag_id, project_id, callback)->
url = "#{settings.apis.tags.url}/user/#{user_id}/tag/#{tag_id}/project/#{project_id}"
request.post {url, timeout: TIMEOUT}, (err, res, body) ->
TagsHandler._handleResponse err, res, {url, user_id, tag_id, project_id}, callback
removeProjectFromAllTags: (user_id, project_id, callback)->
url = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
opts =
url: url
timeout:TIMEOUT
request.del opts, (err, res, body) ->
TagsHandler._handleResponse err, res, {url, user_id, project_id}, callback
_handleResponse: (err, res, params, callback) ->
if err?
params.err = err
logger.err params, "error in tag api"
return callback(err)
else if res? and res.statusCode >= 200 and res.statusCode < 300
return callback(null)
else
err = new Error("tags api returned a failure status code: #{res?.statusCode}")
params.err = err
logger.err params, "tags api returned failure status code: #{res?.statusCode}"
return callback(err)
_requestTags: (user_id, callback)->
opts =
url: "#{settings.apis.tags.url}/user/#{user_id}/tag"
json: true
timeout: TIMEOUT
request.get opts, (err, res, body)->
TagsHandler._handleResponse err, res, {user_id}, (error) ->
return callback(error, []) if error?
callback(null, body or [])
_groupTagsByProject: (tags, callback)->
result = {}
_.each tags, (tag)->
_.each tag.project_ids, (project_id)->
@ -69,7 +92,3 @@ module.exports =
delete clonedTag.project_ids
result[project_id].push(clonedTag)
callback null, result
buildUri = (user_id, project_id)->
uri = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}/tag"

View file

@ -45,7 +45,7 @@ module.exports =
path = "/" + req.params[0] # UpdateMerger expects leading slash
source = req.headers["x-sl-update-source"] or "unknown"
logger.log project_id: project_id, path: path, source: source, "received project contents update"
UpdateMerger.mergeUpdate project_id, path, req, source, (error) ->
UpdateMerger.mergeUpdate null, project_id, path, req, source, (error) ->
return next(error) if error?
res.sendStatus(200)

View file

@ -27,7 +27,7 @@ module.exports =
FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
if shouldIgnore
return callback()
updateMerger.mergeUpdate project._id, path, updateRequest, source, callback
updateMerger.mergeUpdate user_id, project._id, path, updateRequest, source, callback
deleteUpdate: (user_id, projectName, path, source, callback)->

View file

@ -42,6 +42,9 @@ module.exports = TpdsUpdateSender =
_addEntity: (options, callback = (err)->)->
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
if err?
logger.err err:err, options:options, "error getting projects user ids"
return callback(err)
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store"
postOptions =
method : "post"
@ -53,6 +56,9 @@ module.exports = TpdsUpdateSender =
title: "addFile"
streamOrigin : options.streamOrigin
TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)->
if err?
logger.err err:err, project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "error sending file to third party data store queued up for processing"
return callback(err)
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store queued up for processing"
callback(err)

View file

@ -8,7 +8,7 @@ uuid = require('node-uuid')
fs = require('fs')
module.exports =
mergeUpdate: (project_id, path, updateRequest, source, callback = (error) ->)->
mergeUpdate: (user_id, project_id, path, updateRequest, source, callback = (error) ->)->
self = @
logger.log project_id:project_id, path:path, "merging update from tpds"
projectLocator.findElementByPath project_id, path, (err, element)=>
@ -30,7 +30,7 @@ module.exports =
if isFile
self.p.processFile project_id, elementId, fsPath, path, source, callback
else
self.p.processDoc project_id, elementId, fsPath, path, source, callback
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
deleteUpdate: (project_id, path, source, callback)->
projectLocator.findElementByPath project_id, path, (err, element)->
@ -49,14 +49,14 @@ module.exports =
p:
processDoc: (project_id, doc_id, fsPath, path, source, callback)->
processDoc: (project_id, doc_id, user_id, fsPath, path, source, callback)->
readFileIntoTextArray fsPath, (err, docLines)->
if err?
logger.err project_id:project_id, doc_id:doc_id, fsPath:fsPath, "error reading file into text array for process doc update"
return callback(err)
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds"
if doc_id?
editorController.setDoc project_id, doc_id, docLines, source, (err)->
editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)->
callback()
else
setupNewEntity project_id, path, (err, folder, fileName)->

View file

@ -1,21 +1,23 @@
child = require "child_process"
logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics"
fs = require "fs"
Path = require "path"
_ = require("underscore")
ONE_MEG = 1024 * 1024
module.exports = ArchiveManager =
extractZipArchive: (source, destination, _callback = (err) ->) ->
callback = (args...) ->
_callback(args...)
_callback = () ->
timer = new metrics.Timer("unzipDirectory")
logger.log source: source, destination: destination, "unzipping file"
unzip = child.spawn("unzip", [source, "-d", destination])
_isZipTooLarge: (source, callback = (err, isTooLarge)->)->
callback = _.once callback
# don't remove this line, some zips need
# us to listen on this for some unknow reason
unzip = child.spawn("unzip", ["-l", source])
output = ""
unzip.stdout.on "data", (d)->
output += d
error = null
unzip.stderr.on "data", (chunk) ->
@ -29,9 +31,80 @@ module.exports = ArchiveManager =
callback(err)
unzip.on "exit", () ->
timer.done()
if error?
error = new Error(error)
logger.error err:error, source: source, destination: destination, "error unzipping file"
callback(error)
logger.error err:error, source: source, destination: destination, "error checking zip size"
lines = output.split("\n")
lastLine = lines[lines.length - 2]?.trim()
totalSizeInBytes = lastLine?.split(" ")?[0]
totalSizeInBytes = parseInt(totalSizeInBytes)
if !totalSizeInBytes? or isNaN(totalSizeInBytes)
logger.err source:source, "error getting bytes of zip"
return callback(new Error("something went wrong"))
isTooLarge = totalSizeInBytes > (ONE_MEG * 300)
callback(error, isTooLarge)
extractZipArchive: (source, destination, _callback = (err) ->) ->
callback = (args...) ->
_callback(args...)
_callback = () ->
ArchiveManager._isZipTooLarge source, (err, isTooLarge)->
if err?
logger.err err:err, "error checking size of zip file"
return callback(err)
if isTooLarge
return callback(new Error("zip_too_large"))
timer = new metrics.Timer("unzipDirectory")
logger.log source: source, destination: destination, "unzipping file"
unzip = child.spawn("unzip", [source, "-d", destination])
# don't remove this line, some zips need
# us to listen on this for some unknow reason
unzip.stdout.on "data", (d)->
error = null
unzip.stderr.on "data", (chunk) ->
error ||= ""
error += chunk
unzip.on "error", (err) ->
logger.error {err, source, destination}, "unzip failed"
if err.code == "ENOENT"
logger.error "unzip command not found. Please check the unzip command is installed"
callback(err)
unzip.on "exit", () ->
timer.done()
if error?
error = new Error(error)
logger.error err:error, source: source, destination: destination, "error unzipping file"
callback(error)
findTopLevelDirectory: (directory, callback = (error, topLevelDir) ->) ->
fs.readdir directory, (error, files) ->
return callback(error) if error?
if files.length == 1
childPath = Path.join(directory, files[0])
fs.stat childPath, (error, stat) ->
return callback(error) if error?
if stat.isDirectory()
return callback(null, childPath)
else
return callback(null, directory)
else
return callback(null, directory)

View file

@ -4,62 +4,108 @@ _ = require "underscore"
FileTypeManager = require "./FileTypeManager"
EditorController = require "../Editor/EditorController"
ProjectLocator = require "../Project/ProjectLocator"
logger = require("logger-sharelatex")
module.exports = FileSystemImportManager =
addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
fs.readFile path, "utf8", (error, content = "") ->
return callback(error) if error?
content = content.replace(/\r/g, "")
lines = content.split("\n")
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
addFile: (project_id, folder_id, name, path, replace, callback = (error, file)-> )->
if replace
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
addDoc: (user_id, project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
if !isSafe
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add doc is from symlink, stopping process"
return callback("path is symlink")
fs.readFile path, "utf8", (error, content = "") ->
return callback(error) if error?
return callback(new Error("Couldn't find folder")) if !folder?
existingFile = null
for fileRef in folder.fileRefs
if fileRef.name == name
existingFile = fileRef
break
if existingFile?
EditorController.replaceFile project_id, existingFile._id, path, "upload", callback
else
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
else
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
addFolder: (project_id, folder_id, name, path, replace, callback = (error)-> ) ->
EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
return callback(error) if error?
@addFolderContents project_id, new_folder._id, path, replace, (error) ->
return callback(error) if error?
callback null, new_folder
addFolderContents: (project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
fs.readdir folderPath, (error, entries = []) =>
return callback(error) if error?
jobs = _.map entries, (entry) =>
(callback) =>
FileTypeManager.shouldIgnore entry, (error, ignore) =>
content = content.replace(/\r/g, "")
lines = content.split("\n")
if replace
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
return callback(error) if error?
if !ignore
@addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
return callback(new Error("Couldn't find folder")) if !folder?
existingDoc = null
for doc in folder.docs
if doc.name == name
existingDoc = doc
break
if existingDoc?
EditorController.setDoc project_id, existingDoc._id, user_id, lines, "upload", callback
else
callback()
async.parallelLimit jobs, 5, callback
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
else
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
FileTypeManager.isDirectory path, (error, isDirectory) =>
return callback(error) if error?
if isDirectory
@addFolder project_id, folder_id, name, path, replace, callback
addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )->
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
if !isSafe
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add file is from symlink, stopping insert"
return callback("path is symlink")
if !replace
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
else
FileTypeManager.isBinary name, path, (error, isBinary) =>
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
return callback(error) if error?
if isBinary
@addFile project_id, folder_id, name, path, replace, callback
return callback(new Error("Couldn't find folder")) if !folder?
existingFile = null
for fileRef in folder.fileRefs
if fileRef.name == name
existingFile = fileRef
break
if existingFile?
EditorController.replaceFile project_id, existingFile._id, path, "upload", callback
else
@addDoc project_id, folder_id, name, path, replace, callback
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) ->
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
if !isSafe
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add folder is from symlink, stopping insert"
return callback("path is symlink")
EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
return callback(error) if error?
FileSystemImportManager.addFolderContents user_id, project_id, new_folder._id, path, replace, (error) ->
return callback(error) if error?
callback null, new_folder
addFolderContents: (user_id, project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
FileSystemImportManager._isSafeOnFileSystem folderPath, (err, isSafe)->
if !isSafe
logger.log user_id:user_id, project_id:project_id, parent_folder_id:parent_folder_id, folderPath:folderPath, "add folder contents is from symlink, stopping insert"
return callback("path is symlink")
fs.readdir folderPath, (error, entries = []) =>
return callback(error) if error?
jobs = _.map entries, (entry) =>
(callback) =>
FileTypeManager.shouldIgnore entry, (error, ignore) =>
return callback(error) if error?
if !ignore
FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
else
callback()
async.parallelLimit jobs, 5, callback
addEntity: (user_id, project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
if !isSafe
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add entry is from symlink, stopping insert"
return callback("path is symlink")
FileTypeManager.isDirectory path, (error, isDirectory) =>
return callback(error) if error?
if isDirectory
FileSystemImportManager.addFolder user_id, project_id, folder_id, name, path, replace, callback
else
FileTypeManager.isBinary name, path, (error, isBinary) =>
return callback(error) if error?
if isBinary
FileSystemImportManager.addFile user_id, project_id, folder_id, name, path, replace, callback
else
FileSystemImportManager.addDoc user_id, project_id, folder_id, name, path, replace, callback
_isSafeOnFileSystem: (path, callback = (err, isSafe)->)->
fs.lstat path, (err, stat)->
if err?
logger.err err:err, "error with path symlink check"
return callback(err)
isSafe = stat.isFile() or stat.isDirectory()
callback(err, isSafe)

View file

@ -34,7 +34,9 @@ module.exports = ProjectUploadController =
if !name? or name.length == 0 or name.length > 150
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
return res.send success: false
FileSystemImportManager.addEntity project_id, folder_id, name, path, true, (error, entity) ->
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
user_id = req.session.user._id
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
fs.unlink path, ->
timer.done()
if error?

View file

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

View file

@ -13,8 +13,8 @@ module.exports =
RateLimiterMiddlewear.rateLimit({
endpointName: "file-upload"
params: ["Project_id"]
maxRequests: 100
timeInterval: 60 * 20
maxRequests: 200
timeInterval: 60 * 30
}),
SecurityManager.requestCanModifyProject,
ProjectUploadController.uploadFile

View file

@ -1,3 +1,4 @@
UserHandler = require("./UserHandler")
UserDeleter = require("./UserDeleter")
UserLocator = require("./UserLocator")
User = require("../../models/User").User
@ -8,12 +9,9 @@ metrics = require("../../infrastructure/Metrics")
Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager")
UserUpdater = require("./UserUpdater")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
settings = require "settings-sharelatex"
crypto = require "crypto"
module.exports =
module.exports = UserController =
deleteUser: (req, res)->
user_id = req.session.user._id
@ -66,11 +64,18 @@ module.exports =
if err?
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
if err.message == "alread_exists"
message = req.i18n.translate("alread_exists")
message = req.i18n.translate("email_already_registered")
else
message = req.i18n.translate("problem_changing_email_address")
return res.send 500, {message:message}
res.sendStatus(200)
User.findById user_id, (err, user)->
if err?
logger.err err:err, user_id:user_id, "error getting user for email update"
return res.send 500
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
if err?
logger.err err:err, "error populateGroupLicenceInvite"
res.sendStatus(200)
logout : (req, res)->
metrics.inc "user.logout"
@ -85,32 +90,12 @@ module.exports =
if !email? or email == ""
res.sendStatus 422 # Unprocessable Entity
return
logger.log {email}, "registering new user"
UserRegistrationHandler.registerNewUser {
email: email
password: crypto.randomBytes(32).toString("hex")
}, (err, user)->
if err? and err?.message != "EmailAlreadyRegistered"
return next(err)
if err?.message == "EmailAlreadyRegistered"
logger.log {email}, "user already exists, resending welcome email"
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
return next(err) if err?
setNewPasswordUrl = "#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}&email=#{encodeURIComponent(email)}"
EmailHandler.sendEmail "registered", {
to: user.email
setNewPasswordUrl: setNewPasswordUrl
}, () ->
res.json {
email: user.email
setNewPasswordUrl: setNewPasswordUrl
}
UserRegistrationHandler.registerNewUserAndSendActivationEmail email, (error, user, setNewPasswordUrl) ->
return next(error) if error?
res.json {
email: user.email
setNewPasswordUrl: setNewPasswordUrl
}
changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change"

View file

@ -0,0 +1,26 @@
SubscriptionDomainHandler = require("../Subscription/SubscriptionDomainHandler")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
SubscriptionGroupHandler = require("../Subscription/SubscriptionGroupHandler")
logger = require("logger-sharelatex")
module.exports = UserHandler =
populateGroupLicenceInvite: (user, callback)->
logger.log user_id:user._id, "populating any potential group licence invites"
licence = SubscriptionDomainHandler.getLicenceUserCanJoin user
if !licence?
return callback()
SubscriptionGroupHandler.isUserPartOfGroup user._id, licence.subscription_id, (err, alreadyPartOfGroup)->
if err?
return callback(err)
else if alreadyPartOfGroup
logger.log user_id:user._id, "user already part of group, not creating notifcation for them"
return callback()
else
NotificationsBuilder.groupPlan(user, licence).create(callback)
setupLoginData: (user, callback = ->)->
@populateGroupLicenceInvite user, callback

View file

@ -1,4 +1,6 @@
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
ErrorController = require("../Errors/ErrorController")
logger = require("logger-sharelatex")
Settings = require("settings-sharelatex")
fs = require('fs')
@ -21,10 +23,32 @@ module.exports =
newTemplateData: newTemplateData
new_email:req.query.new_email || ""
activateAccountPage: (req, res) ->
# An 'activation' is actually just a password reset on an account that
# was set with a random password originally.
if !req.query?.user_id? or !req.query?.token?
return ErrorController.notFound(req, res)
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
return next(error) if error?
if !user
return ErrorController.notFound(req, res)
if user.loginCount > 0
# Already seen this user, so account must be activate
# This lets users keep clicking the 'activate' link in their email
# as a way to log in which, if I know our users, they will.
res.redirect "/login?email=#{encodeURIComponent(user.email)}"
else
res.render 'user/activate',
title: 'activate_account'
email: user.email,
token: req.query.token
loginPage : (req, res)->
res.render 'user/login',
title: 'login',
redir: req.query.redir
redir: req.query.redir,
email: req.query.email
settingsPage : (req, res, next)->
logger.log user: req.session.user, "loading settings page"

View file

@ -5,8 +5,12 @@ AuthenticationManager = require("../Authentication/AuthenticationManager")
NewsLetterManager = require("../Newsletter/NewsletterManager")
async = require("async")
logger = require("logger-sharelatex")
crypto = require("crypto")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
settings = require "settings-sharelatex"
module.exports =
module.exports = UserRegistrationHandler =
validateEmail : (email) ->
re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(email)
@ -59,6 +63,31 @@ module.exports =
logger.log user: user, "registered"
callback(err, user)
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
logger.log {email}, "registering new user"
UserRegistrationHandler.registerNewUser {
email: email
password: crypto.randomBytes(32).toString("hex")
}, (err, user)->
if err? and err?.message != "EmailAlreadyRegistered"
return callback(err)
if err?.message == "EmailAlreadyRegistered"
logger.log {email}, "user already exists, resending welcome email"
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
return callback(err) if err?
setNewPasswordUrl = "#{settings.siteUrl}/user/activate?token=#{token}&user_id=#{user._id}"
EmailHandler.sendEmail "registered", {
to: user.email
setNewPasswordUrl: setNewPasswordUrl
}, () ->
callback null, user, setNewPasswordUrl

View file

@ -23,9 +23,9 @@ for path in [
"#{jsPath}main.js",
"#{jsPath}libs.js",
"#{jsPath}ace/ace.js",
"#{jsPath}libs/pdfjs-1.0.1040/pdf.js",
"#{jsPath}libs/pdfjs-1.0.1040/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.0.1040/compatibility.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
"/stylesheets/style.css"
]
filePath = Path.join __dirname, "../../../", "public#{path}"

View file

@ -31,6 +31,7 @@ ProjectSchema = new Schema
description : {type:String, default:''}
archived : { type: Boolean }
deletedDocs : [DeletedDocSchema]
imageName : { type: String }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id?
@ -65,51 +66,7 @@ ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)
this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=>
callback(err, projects, collabertions, readOnlyProjects)
sanitizeTypeOfElement = (elementType)->
lastChar = elementType.slice -1
if lastChar != "s"
elementType +="s"
if elementType == "files"
elementType = "fileRefs"
return elementType
ProjectSchema.statics.putElement = (project_id, folder_id, element, type, callback)->
if !element?
e = new Error("no element passed to be inserted")
logger.err project_id:project_id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null"
return callback(e)
type = sanitizeTypeOfElement type
this.findById project_id, (err, project)=>
if err?
callback(err)
if !folder_id?
folder_id = project.rootFolder[0]._id
require('../Features/Project/ProjectLocator').findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=>
newPath =
fileSystem: "#{path.fileSystem}/#{element.name}"
mongo: path.mongo # TODO: This is not correct
if err?
callback(err)
logger.log project_id: project_id, element_id: element._id, fileType: type, folder_id: folder_id, "adding element to project"
id = element._id+''
element._id = concreteObjectId(id)
conditions = _id:project_id
mongopath = "#{path.mongo}.#{type}"
update = "$push":{}
update["$push"][mongopath] = element
this.update conditions, update, {}, (err)->
if(err)
logger.err err: err, project: project, 'error saving in putElement project'
if callback?
callback(err, {path:newPath})
getIndexOf = (searchEntity, id)->
length = searchEntity.length
count = 0
while(count < length)
if searchEntity[count]._id+"" == id+""
return count
count++

View file

@ -34,6 +34,8 @@ UserSchema = new Schema
github: { type:Boolean, default: Settings.defaultFeatures.github }
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references }
}
featureSwitches : {
pdfng: { type: Boolean }

View file

@ -16,6 +16,7 @@ ReferalController = require('./Features/Referal/ReferalController')
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController")
NotificationsController = require("./Features/Notifications/NotificationsController")
CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter')
UserInfoController = require('./Features/User/UserInfoController')
UserController = require("./Features/User/UserController")
@ -37,6 +38,7 @@ RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
logger = require("logger-sharelatex")
_ = require("underscore")
@ -77,6 +79,8 @@ module.exports = class Router
webRouter.get '/blog', BlogController.getIndexPage
webRouter.get '/blog/*', BlogController.getPage
webRouter.get '/user/activate', UserPagesController.activateAccountPage
webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
@ -128,8 +132,15 @@ module.exports = class Router
webRouter.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
webRouter.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate
webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
webRouter.post '/tag', AuthenticationController.requireLogin(), TagsController.createTag
webRouter.post '/tag/:tag_id/rename', AuthenticationController.requireLogin(), TagsController.renameTag
webRouter.delete '/tag/:tag_id', AuthenticationController.requireLogin(), TagsController.deleteTag
webRouter.post '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.addProjectToTag
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
# Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
@ -168,6 +179,9 @@ module.exports = class Router
webRouter.get /learn(\/.*)?/, WikiController.getPage
webRouter.post "/project/:Project_id/references/index", SecurityManager.requestCanAccessProject, ReferencesController.index
webRouter.post "/project/:Project_id/references/indexAll", SecurityManager.requestCanAccessProject, ReferencesController.indexAll
#Admin Stuff
webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
webRouter.get '/admin/user', SecurityManager.requestIsAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon

View file

@ -53,7 +53,13 @@ block content
include ./editor/share
#ide-body(ng-cloak, layout="main", ng-hide="state.loading", resize-on="layout:chat:resize")
#ide-body(
ng-cloak,
layout="main",
ng-hide="state.loading",
resize-on="layout:chat:resize",
minimum-restore-size-west="130"
)
.ui-layout-west
include ./editor/file-tree
@ -76,7 +82,7 @@ block content
ng-click="done()"
) &times;
h3 {{ title }}
.modal-body {{ message }}
.modal-body(ng-bind-html="message")
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")}
@ -95,13 +101,13 @@ block content
"paths" : {
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
"moment": "libs/moment-2.7.0",
"libs/pdf": "libs/pdfjs-1.0.1040/pdf"
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
"shim": {
"libs/pdf": {
deps: ["libs/pdfjs-1.0.1040/compatibility"]
deps: ["libs/pdfjs-1.3.91/compatibility"]
},
"ace/ext-searchbox": {
deps: ["ace/ace"]
@ -120,7 +126,7 @@ block content
- locals.suppressDefaultJs = true
- var pdfPath = 'libs/pdfjs-1.0.1040/pdf.worker.js'
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
script(type='text/javascript').

View file

@ -6,6 +6,7 @@ div.full-size(
resize-on="layout:main:resize"
resize-proportionally="true"
initial-size-east="'50%'"
minimum-restore-size-east="300"
)
.ui-layout-center
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
@ -19,6 +20,7 @@ div.full-size(
keybindings="settings.mode",
font-size="settings.fontSize",
auto-complete="settings.autoComplete",
spell-check="true",
spell-check-language="project.spellCheckLanguage",
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
show-print-margin="false",

View file

@ -1,4 +1,4 @@
aside#file-tree(ng-controller="FileTreeController").full-size
aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }").full-size
.toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write")
a(
href,
@ -27,7 +27,8 @@ aside#file-tree(ng-controller="FileTreeController").full-size
href,
ng-click="startRenamingSelected()",
tooltip="#{translate('rename')}",
tooltip-placement="bottom"
tooltip-placement="bottom",
ng-show="multiSelectedCount == 0"
)
i.fa.fa-pencil
a(
@ -45,26 +46,24 @@ aside#file-tree(ng-controller="FileTreeController").full-size
ng-controller="FileTreeRootFolderController",
ng-class="{ 'no-toolbar': !permissions.write }"
)
div(ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')")
ul.list-unstyled.file-tree-list
li(
ng-class="{ 'selected': ui.view == 'pdf' }"
ng-controller="PdfViewToggleController"
)
.entity
.entity-name(
ng-click="togglePdfView()"
)
i.fa.fa-fw.toggle
i.fa.fa-fw.fa-file-pdf-o
| PDF
ul.list-unstyled.file-tree-list(
droppable="permissions.write"
accept=".entity-name"
on-drop-callback="onDrop"
)
li(
ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')"
ng-class="{ 'selected': ui.view == 'pdf' }"
ng-controller="PdfViewToggleController"
)
.entity
.entity-name(
ng-click="togglePdfView()"
)
i.fa.fa-fw.toggle
i.fa.fa-fw.fa-file-pdf-o
| PDF
file-entity(
entity="entity",
permissions="permissions",
@ -81,7 +80,7 @@ aside#file-tree(ng-controller="FileTreeController").full-size
)
.entity
.entity-name(
ng-click="select()"
ng-click="select($event)"
)
//- Just a spacer to align with folders
i.fa.fa-fw.toggle
@ -89,19 +88,17 @@ aside#file-tree(ng-controller="FileTreeController").full-size
span {{ entity.name }}
script(type='text/ng-template', id='entityListItemTemplate')
li(
ng-class="{ 'selected': entity.selected }",
ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }",
ng-controller="FileTreeEntityController"
)
.entity(ng-if="entity.type != 'folder'")
.entity-name(
ng-click="select()"
ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write"
draggable-helper="draggableHelper"
context-menu
data-target="context-menu-{{ entity.id }}"
context-menu-container="body"
@ -112,7 +109,6 @@ script(type='text/ng-template', id='entityListItemTemplate')
i.fa.fa-fw.fa-file(ng-if="entity.type == 'doc'")
i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'")
span(
ng-hide="entity.renaming"
) {{ entity.name }}
@ -126,12 +122,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()"
)
span.dropdown(
span.dropdown.entity-menu-toggle(
dropdown,
ng-show="entity.selected",
ng-if="permissions.write"
)
a.dropdown-toggle(href, dropdown-toggle)
a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right
@ -140,12 +135,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href
ng-click="startRenaming()"
right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")}
li
a(
href
ng-click="openDeleteModal()"
right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")}
div.dropdown.context-menu(
@ -158,20 +155,23 @@ script(type='text/ng-template', id='entityListItemTemplate')
href
ng-click="startRenaming()"
right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")}
li
a(
href
ng-click="openDeleteModal()"
right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")}
.entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController")
.entity-name(
ng-click="select()"
ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write"
draggable-helper="draggableHelper"
droppable="permissions.write"
accept=".entity-name"
on-drop-callback="onDrop"
@ -194,7 +194,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
'fa-folder': !expanded, \
'fa-folder-open': expanded \
}"
ng-click="select()"
ng-click="select($event)"
)
span(
@ -210,12 +210,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()"
)
span.dropdown(
span.dropdown.entity-menu-toggle(
dropdown,
ng-if="permissions.write"
ng-show="entity.selected"
)
a.dropdown-toggle(href, dropdown-toggle)
a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right
@ -224,12 +223,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href
ng-click="startRenaming()"
right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")}
li
a(
href
ng-click="openDeleteModal()"
right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")}
li.divider
li
@ -261,12 +262,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href
ng-click="startRenaming()"
right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")}
li
a(
href
ng-click="openDeleteModal()"
right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")}
li.divider
li
@ -306,6 +309,7 @@ script(type='text/ng-template', id='newDocModalTemplate')
h3 #{translate("new_file")}
.modal-body
form(novalidate, name="newDocForm")
div.alert.alert-danger(ng-if="error") {{error}}
input.form-control(
type="text",
placeholder="File Name",
@ -330,6 +334,7 @@ script(type='text/ng-template', id='newFolderModalTemplate')
.modal-header
h3 #{translate("new_folder")}
.modal-body
div.alert.alert-danger(ng-if="error") {{error}}
form(novalidate, name="newFolderForm")
input.form-control(
type="text",
@ -354,9 +359,20 @@ script(type='text/ng-template', id='newFolderModalTemplate')
script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-header
h3 #{translate("upload_files")}
span &nbsp;
.alert.alert-warning.small(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
.alert.alert-warning.small(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
.alert.alert-warning.small.modal-alert(ng-if="conflicts.length > 0")
p.text-center
| The following files already exist in this project:
ul.text-center.list-unstyled.row-spaced-small
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
p.text-center.row-spaced-small
| Do you want to overwrite them?
p.text-center
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
| &nbsp;
a(href, ng-click="cancel()").btn.btn-default Cancel
.modal-body(
fine-upload
@ -367,10 +383,14 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
drag-area-text="{{drag_files}}"
hint-text="{{hint_press_and_hold_control_key}}"
multiple="true"
auto-upload="false"
on-complete-callback="onComplete"
on-upload-callback="onUpload"
on-validate-batch="onValidateBatch"
on-error-callback="onError"
on-submit-callback="onSubmit"
on-cancel-callback="onCancel"
control="control"
params="{'folder_id': parent_folder_id}"
)
span #{translate("upload_files")}
@ -382,6 +402,8 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
h3 #{translate("delete")} {{ entity.name }}
.modal-body
p !{translate("sure_you_want_to_delete")}
ul
li(ng-repeat="entity in entities") {{entity.name}}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"

View file

@ -50,16 +50,22 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
.hotkey
span.combination {{ctrl}} + A
span.description Select All
.col-xs-6
.hotkey
span.combination Tab
span.description Indent Selection
.col-xs-6
.hotkey
span.combination {{ctrl}} + U
span.combination Ctrl + U
span.description To Uppercase
.hotkey
span.combination Ctrl + Shift + U
span.description To Lowercase
.hotkey
span.combination {{ctrl}} + B
span.description Bold text
.hotkey
span.combination {{ctrl}} + I
span.description Italic Text
.modal-footer
button.btn.btn-default(
ng-click="cancel()"

View file

@ -1,16 +1,34 @@
div.full-size.pdf(ng-controller="PdfController")
.toolbar.toolbar-tall
a.btn.btn-info(
href,
ng-disabled="pdf.compiling",
ng-click="recompile()"
)
i.fa.fa-refresh(
ng-class="{'fa-spin': pdf.compiling }"
.btn-group(dropdown)
a.btn.btn-info(
href,
ng-disabled="pdf.compiling",
ng-click="recompile()"
)
| &nbsp;&nbsp;
span(ng-show="!pdf.compiling") #{translate("recompile")}
span(ng-show="pdf.compiling") #{translate("compiling")}...
i.fa.fa-refresh(
ng-class="{'fa-spin': pdf.compiling }"
)
| &nbsp;&nbsp;
span(ng-show="!pdf.compiling") #{translate("recompile")}
span(ng-show="pdf.compiling") #{translate("compiling")}...
a.btn.btn-info.dropdown-toggle(
href,
ng-disabled="pdf.compiling",
dropdown-toggle
)
span.caret
ul.dropdown-menu.dropdown-menu-right
li.dropdown-header #{translate("compile_mode")}
li
a(href, ng-click="draft = false")
i.fa.fa-fw(ng-class="{'fa-check': !draft}")
| &nbsp;#{translate("normal")}
li
a(href, ng-click="draft = true")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| &nbsp;#{translate("fast")}&nbsp;
span.subdued [draft]
a.log-btn(
href
ng-click="toggleLogs()"
@ -94,41 +112,43 @@ div.full-size.pdf(ng-controller="PdfController")
| #{translate("learn_how_to_make_documents_compile_quickly")}
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
p
strong #{translate("upgrade_for_faster_compiles")}
h5 #{translate("free_accounts_have_timeout_upgrade_to_increase")}
h4 Plus:
h4
p
strong #{translate("upgrade_for_faster_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p Plus:
p
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
h4
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
h4
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
sixpack-convert="track_changes_feature_info"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
p(ng-controller="FreeTrialModalController")
a.btn.btn-success.row-spaced-small(
href
ng-class="buttonClass"
sixpack-convert="track_changes_feature_info"
ng-click="startFreeTrial('compile-timeout')"
) #{translate("start_free_trial")}
.pdf-errors(ng-show="pdf.projectTooLarge")
.alert.alert-danger

View file

@ -6,46 +6,84 @@ script(type="text/ng-template", id="publishProjectAsTemplateModalTemplate")
ng-click="cancel()"
) &times;
h3 #{translate("publish_as_template")}
.modal-body.modal-body-share
span(ng-hide="problemTalkingToTemplateApi")
form()
label(for='Description') #{translate("template_description")}
.form-group
textarea.form-control(
rows=5,
name='Description',
ng-model="templateDetails.description",
value=""
)
div(ng-show="templateDetails.exists").text-center.templateDetails
| #{translate("project_last_published_at")}
strong {{templateDetails.publishedDate}}.
a(ng-href="{{templateDetails.canonicalUrl}}") #{translate("view_in_template_gallery")}.
div(ng-if="project.features.templates")
.modal-body.modal-body-share
span(ng-hide="problemTalkingToTemplateApi")
form()
label(for='Description') #{translate("template_description")}
.form-group
textarea.form-control(
rows=5,
name='Description',
ng-model="templateDetails.description",
value=""
)
div(ng-show="templateDetails.exists").text-center.templateDetails
| #{translate("project_last_published_at")}
strong {{templateDetails.publishedDate}}.
a(ng-href="{{templateDetails.canonicalUrl}}") #{translate("view_in_template_gallery")}.
span(ng-show="problemTalkingToTemplateApi") #{translate("problem_talking_to_publishing_service")}.
span(ng-show="problemTalkingToTemplateApi") #{translate("problem_talking_to_publishing_service")}.
.modal-footer(ng-hide="problemTalkingToTemplateApi")
button.btn.btn-default(
ng-click="cancel()",
ng-disabled="state.publishInflight || state.unpublishInflight"
)
span #{translate("cancel")}
button.btn.btn-info(
ng-click="unpublishTemplate()",
ng-disabled="state.publishInflight || state.unpublishInflight"
ng-show="templateDetails.exists"
)
span(ng-show="!state.unpublishInflight") #{translate("unpublish")}
span(ng-show="state.unpublishInflight") #{translate("unpublishing")}...
button.btn.btn-primary(
ng-click="publishTemplate()",
ng-disabled="state.publishInflight || state.unpublishInflight"
)
span(ng-show="!state.publishInflight && !templateDetails.exists") #{translate("publish")}
span(ng-show="!state.publishInflight && templateDetails.exists") #{translate("republish")}
span(ng-show="state.publishInflight") #{translate("publishing")}...
.modal-footer(ng-hide="problemTalkingToTemplateApi")
button.btn.btn-default(
ng-click="cancel()",
ng-disabled="state.publishInflight || state.unpublishInflight"
)
span #{translate("cancel")}
div(ng-hide="project.features.templates")
.modal-body.modal-body-share
p #{translate("upgrade_to_get_feature", {feature:"templates"})}
button.btn.btn-info(
ng-click="unpublishTemplate()",
ng-disabled="state.publishInflight || state.unpublishInflight"
ng-show="templateDetails.exists"
)
span(ng-show="!state.unpublishInflight") #{translate("unpublish")}
span(ng-show="state.unpublishInflight") #{translate("unpublishing")}...
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
button.btn.btn-primary(
ng-click="publishTemplate()",
ng-disabled="state.publishInflight || state.unpublishInflight"
)
span(ng-show="!state.publishInflight && !templateDetails.exists") #{translate("publish")}
span(ng-show="!state.publishInflight && templateDetails.exists") #{translate("republish")}
span(ng-show="state.publishInflight") #{translate("publishing")}...
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
.modal-footer(ng-controller="FreeTrialModalController")
.text-center
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('templates')"
) #{translate("start_free_trial")}
.row-spaced-small.small(ng-show="startedFreeTrial")
| #{translate("refresh_page_after_starting_free_trial")}

View file

@ -79,33 +79,36 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
type="submit"
ng-mousedown="addMembers()"
) #{translate("share")}
div.text-center(ng-hide="canAddCollaborators")
p #{translate("need_to_upgrade_for_more_collabs")}.
h4
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
div(ng-hide="canAddCollaborators")
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
.row
.col-md-8.col-md-offset-2
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
h4
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
h4
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
h4
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h4
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
h4
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController")
p.text-center.row-spaced-thin(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
@ -122,7 +125,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-primary(
ng-click="done()"
) #{translate("done")}
) #{translate("close")}
script(type="text/ng-template", id="makePublicModalTemplate")
.modal-header

View file

@ -1,83 +1,39 @@
div#trackChanges(ng-show="ui.view == 'track-changes'")
span(ng-controller="TrackChangesPremiumPopup")
span(ng-if="versioningPopupType == 'default'")
.upgrade-prompt(ng-show="!project.features.versioning")
.message(ng-show="project.owner._id == user.id")
p #{translate("upgrade_to_get_feature", {feature:"Entire Doc History"})}
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
h4
.upgrade-prompt(ng-show="!project.features.versioning")
.message(ng-show="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
ul.list-unstyled
li
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
h4
li
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h4
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
h4
li
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('track-changes')"
) #{translate("start_free_trial")}
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
p
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
.upgrade-prompt(ng-show="!project.features.versioning", ng-if="versioningPopupType == 'discount'")
.message(ng-show="project.owner._id == user.id")
p #{translate("upgrade_to_get_feature", {feature:"Entire Doc History"})}
h2(style="color:#a93529;") 20% Off First 6 Months!
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
h4
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
h4
i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
h4
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
h4
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h4
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
h4
i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController")
p.text-center(ng-controller="FreeTrialModalController")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('track-changes', 'cf3yutfzu7ztxz')"
ng-click="startFreeTrial('track-changes')"
) #{translate("start_free_trial")}

View file

@ -9,7 +9,8 @@ block content
script(type="text/javascript").
window.data = {
projects: !{JSON.stringify(projects).replace(/\//g, '\\/')},
tags: !{JSON.stringify(tags).replace(/\//g, '\\/')}
tags: !{JSON.stringify(tags).replace(/\//g, '\\/')},
notifications: !{JSON.stringify(notifications).replace(/\//g, '\\/')}
};
window.algolia = {
institutions: {
@ -20,12 +21,14 @@ block content
.content.content-alt(ng-controller="ProjectPageController")
.container
.row(ng-cloak)
span(ng-show="first_sign_up == 'default' || projects.length > 0")
aside.col-md-2.col-xs-3
include ./list/side-bar
.col-md-10.col-xs-9
include ./list/notifications
include ./list/project-list
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")

View file

@ -18,6 +18,8 @@ script(type='text/ng-template', id='newTagModalTemplate')
stop-propagation="click"
)
.modal-footer
.modal-footer-left
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
//- We stop propagation to stop the clicks from closing the
//- 'move to folder' menu.
button.btn.btn-default(
@ -25,10 +27,67 @@ script(type='text/ng-template', id='newTagModalTemplate')
stop-propagation="click"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="newTagForm.$invalid"
ng-disabled="newTagForm.$invalid || state.inflight"
ng-click="create()"
stop-propagation="click"
) #{translate("create")}
)
span(ng-show="!state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='deleteTagModalTemplate')
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 #{translate("delete_folder")}
.modal-body
p #{translate("about_to_delete_folder")}
ul
li
strong {{tag.name}}
.modal-footer
.modal-footer-left
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-default(
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-danger(
ng-click="delete()",
ng-disabled="state.inflight"
)
span(ng-show="state.inflight") #{translate("deleting")}...
span(ng-show="!state.inflight") #{translate("delete")}
script(type='text/ng-template', id='renameTagModalTemplate')
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="cancel()"
) &times;
h3 #{translate("rename_folder")}
.modal-body
form(name="renameTagForm", novalidate)
input.form-control(
type="text",
placeholder="Tag Name",
ng-model="inputs.tagName",
required,
on-enter="rename()",
focus-on="open"
)
.modal-footer
.modal-footer-left
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
button.btn.btn-primary(
ng-click="rename()",
ng-disabled="renameTagForm.$invalid || state.inflight"
)
span(ng-show="!state.inflight") #{translate("rename")}
span(ng-show="state.inflight") #{translate("renaming")}...
script(type='text/ng-template', id='renameProjectModalTemplate')
.modal-header

View file

@ -0,0 +1,15 @@
span(ng-controller="NotificationsController").userNotifications
ul.list-unstyled.notifications-list(
ng-if="notifications.length > 0",
ng-cloak
)
li.notification_entry(
ng-repeat="unreadNotification in notifications",
)
.row(ng-hide="unreadNotification.hide")
.col-xs-12
.alert.alert-info
span(ng-bind-html="unreadNotification.html")
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}

View file

@ -58,7 +58,7 @@
)
li.dropdown-header #{translate("add_to_folder")}
li(
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
ng-repeat="tag in tags | orderBy:'name'",
ng-controller="TagDropdownItemController"
)
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")

View file

@ -37,22 +37,23 @@
ul.list-unstyled.folders-menu(
ng-controller="TagListController"
)
li(ng-class="{active: (filter == 'all')}")
a(href, ng-click="filterProjects('all')") #{translate("all_projects")}
li(ng-class="{active: (filter == 'owned')}")
a(href, ng-click="filterProjects('owned')") #{translate("your_projects")}
li(ng-class="{active: (filter == 'shared')}")
a(href, ng-click="filterProjects('shared')") #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}")
a(href, ng-click="filterProjects('archived')") #{translate("deleted_projects")}
li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')")
a(href) #{translate("all_projects")}
li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')")
a(href) #{translate("your_projects")}
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
a(href) #{translate("shared_with_you")}
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
a(href) #{translate("deleted_projects")}
li
h2 #{translate("folders")}
li(
ng-repeat="tag in tags | filter:nonEmpty",
li.tag(
ng-repeat="tag in tags | orderBy:name",
ng-class="{active: tag.selected}",
ng-cloak
ng-cloak,
ng-click="selectTag(tag)"
)
a.tag(href, ng-click="selectTag(tag)")
a.tag-name(href)
i.icon.fa.fa-fw(
ng-class="{\
'fa-folder-open-o': tag.selected,\
@ -61,6 +62,23 @@
)
span.name {{tag.name}}
span.subdued ({{tag.project_ids.length}})
span.dropdown.tag-menu(dropdown)
a.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle,
stop-propagation="click"
)
span.caret
ul.dropdown-menu.dropdown-menu-right(
role="menu"
)
li
a(href, ng-click="renameTag(tag)", stop-propagation="click")
| #{translate("rename")}
li
a(href, ng-click="deleteTag(tag)", stop-propagation="click")
| #{translate("delete")}
li(ng-cloak)
a.tag(href, ng-click="openNewTagModal()")
i.fa.fa-fw.fa-plus
@ -102,41 +120,51 @@
) #{translate("complete")}
.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgrade-reason").text-centered
.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
span(sixpack-default).text-centered
hr
p.small #{translate("on_free_sl")}
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(sixpack-when="dropbox").text-centered
hr
.card.card-thin
span(sixpack-when="random").text-centered
span(ng-if="randomView == 'default'")
hr
p.small #{translate("on_free_sl")}
p
span Get Dropbox Sync
p
img(src="/img/dropbox/simple_logo.png")
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(sixpack-when="github").text-centered
hr
.card.card-thin
p
span Get Github Sync
p
img(src="/img/github/octocat.jpg")
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(ng-if="randomView == 'dropbox'")
hr
.card.card-thin
p
span Get Dropbox Sync
p
img(src="/img/dropbox/simple_logo.png")
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
span(ng-if="randomView == 'github'")
hr
.card.card-thin
p
span Get Github Sync
p
img(src="/img/github/octocat.jpg")
p
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
p.small.text-centered
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
script.
window.userHasSubscription = #{settings.enableSubscriptions && !hasSubscription}

View file

@ -32,8 +32,8 @@
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
'NS_ERROR_NOT_CONNECTED:',
"TypeError: Cannot read property 'row' of undefined",
'/NS_ERROR_NOT_CONNECTED/i',
"/Cannot read property 'row' of undefined/i",
'TypeError: start is undefined'
],
ignoreUrls: [
@ -57,7 +57,7 @@
],
shouldSendCallback: function(data) {
// only send a fraction of errors
var sampleRate = 1.00;
var sampleRate = 0.01;
return (Math.random() <= sampleRate);
},
dataCallback: function(data) {

View file

@ -8,7 +8,7 @@ block content
.card
.page-header
h1 #{translate("your_subscription")}
div To make changes to your subscription please contact team@sharelatex.com
div To make changes to your subscription please contact accounts@sharelatex.com
div &nbsp;
div
-if(subscription.groupPlan)

View file

@ -6,7 +6,7 @@ block scripts
window.recomendedCurrency = '#{recomendedCurrency}'
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
window.taxRate = #{taxRate}
window.subscription = !{JSON.stringify(subscription)}
@ -36,32 +36,32 @@ mixin printPlans(plans)
mixin printPlan(plan)
block content
.content.content-alt
.container
.content.content-alt(ng-cloak)
.container(ng-controller="UserSubscriptionController")
.row
.col-md-8.col-md-offset-2
.card
.card(ng-if="view == 'overview'")
.page-header
h1 #{translate("your_subscription")}
-if (groups.length != 0)
each groupSubscription in groups
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
span
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
- else if (subscription)
case subscription.state
when "free-trial"
p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})}
p !{translate("choose_a_plan_below")}
when "active"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
a(href, ng-click="changePlan = true") !{translate("change_plan")}.
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
p.pull-right
p: form(action="/user/subscription/cancel",method="post")
input(type="hidden", name="_csrf", value=csrfToken)
p
a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")}
| &nbsp;
input(type="submit", value="Cancel your subscription").btn.btn-primary#cancelSubscription
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
when "canceled"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
@ -76,7 +76,7 @@ block content
p !{translate("problem_with_subscription_contact_us")}
-if(subscription.groupPlan)
a(href="/subscription/group").btn.btn-success !{translate("manage_group")}
a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
@ -105,17 +105,50 @@ block content
mixin printPlans(plans.individualMonthlyPlans)
mixin printPlans(plans.individualAnnualPlans)
.card(ng-if="view == 'cancelation'")
.page-header
h1 #{translate("Cancel Subscription")}
span(ng-if="sixpackOpt == 'downgrade-options'")
div(ng-show="showExtendFreeTrial", style="text-align: center")
p !{translate("have_more_days_to_try", {days:14})}
button(type="submit", ng-click="exendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
p
| &nbsp;
p
a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
div(ng-show="showDowngradeToStudent", style="text-align: center")
span(ng-controller="ChangePlanFormController")
p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})}
button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-success #{translate("yes_please")}
p
| &nbsp;
p
a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
div(ng-show="showBasicCancel")
p #{translate("sure_you_want_to_cancel")}
a(href="/project").btn.btn-info #{translate("i_want_to_stay")}
| &nbsp;
a(ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
span(ng-if="sixpackOpt == 'basic'")
p #{translate("sure_you_want_to_cancel")}
a(href="/project").btn.btn-info #{translate("i_want_to_stay")}
| &nbsp;
a(ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
script(type="text/javascript").
$('#cancelSubscription').on("click", function() {
ga('send', 'event', 'subscription-funnel', 'cancelation')
})
script(type='text/ng-template', id='confirmChangePlanModalTemplate')
.modal-header
h3 Change plan?
h3 #{translate("change_plan")}
.modal-body
p !{translate("sure_you_want_to_change_plan", {planName:"<strong>{{plan.name}}</strong>"})}
.modal-footer
@ -131,4 +164,22 @@ block content
span(ng-show="inflight") #{translate("processing")}...
script(type='text/ng-template', id='LeaveGroupModalTemplate')
.modal-header
h3 #{translate("leave_group")}
.modal-body
p #{translate("sure_you_want_to_leave_group")}
.modal-footer
button.btn.btn-default(
ng-disabled="inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-danger(
ng-disabled="state.inflight"
ng-click="confirmLeaveGroup()"
)
span(ng-hide="inflight") #{translate("leave_now")}
span(ng-show="inflight") #{translate("processing")}...

View file

@ -112,7 +112,6 @@ block content
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
option(value="", disabled, selected) Year
option(value="2015") 2015
option(value="2016") 2016
option(value="2017") 2017
option(value="2018") 2018

View file

@ -3,6 +3,12 @@ block scripts
script(type='text/javascript').
window.recomendedCurrency = '#{recomendedCurrency}'
window.abCurrencyFlag = '#{abCurrencyFlag}'
script(type='text/javascript').
(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true;
s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js';
var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})();
block content
.content-alt
.content.plans(ng-controller="PlansController")
@ -186,30 +192,30 @@ block content
.modal-header
h3 #{translate("group_plan_enquiry")}
.modal-body
form(name='form1', autocomplete='off', enctype='multipart/form-data', method='post', novalidate='', action='https://sharelatex.wufoo.com/forms/z7x3p3/#public', _lpchecked='1')
.form-group
label(for='Field9') #{translate("name")}
input.form-control(name='Field9', type='text', value='', maxlength='255', tabindex='1', onkeyup='')
.form-group
label(for='Field11') #{translate("email")}
input.form-control(name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
.form-group
label(for='Field12') #{translate("university")}
input.form-control(name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
.form-group
label(for='Field13') #{translate("position")}
input.form-control(name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group
input.btn.btn-primary.btn-large(name='saveForm', type='submit', value='Send')
div(style='display: none;')
label(for='comment') Do Not Fill This Out
textarea#comment(name='comment', rows='1', cols='1')
input#idstamp(type='hidden', name='idstamp', value='xkgLkZnS/AQW71jCS1d0XrrFjq26lJryIPVk2rx0YkU=')
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
span(ng-show="sent == false")
.form-group
label#title9(for='Field9')
| Name
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
label#title11.desc(for='Field11')
| Email
.form-group
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
label#title12.desc(for='Field12')
| University / Company
.form-group
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
label#title13.desc(for='Field13')
| Position
.form-group
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
.form-group
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';")
.form-group.text-center
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
span(ng-show="sent")
p Request Sent, Thank you.
.row
.col-md-12

View file

@ -5,14 +5,18 @@ block content
.container
.row
.col-md-8.col-md-offset-2
.card
.card(ng-cloak)
.page-header
h2 #{translate("thanks_for_subscribing")}
.alert.alert-success
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>"+subscription.price+"</strong>", collectionDate:"<strong>"+subscription.nextPaymentDueAt+"</strong>"})}
p #{translate("if_you_dont_want_to_be_charged")}
a(href="/user/subscription") #{translate("click_here_to_cancel")}.
span(sixpack-switch="upgrade-success-message")
span(sixpack-default)
p #{translate("if_you_dont_want_to_be_charged")}
a(href="/user/subscription") #{translate("click_here_to_cancel")}.
span(sixpack-when="manage-subscription")
p #{translate("to_modify_your_subscription_go_to")}
a(href="/user/subscription") #{translate("manage_subscription")}.
p
- if (subscription.groupPlan == true)
a.btn.btn-success.btn-large(href="/subscription/group") #{translate("add_your_first_group_member_now")}

View file

@ -0,0 +1,64 @@
extends ../layout
block content
.content.content-alt
.container
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.alert.alert-success #{translate("nearly_activated")}
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.card
.page-header
h1 #{translate("please_set_a_password")}
form(
async-form="activate",
name="activationForm",
action="/user/password/set",
method="POST",
ng-cloak
)
input(name='_csrf', type='hidden', value=csrfToken)
input(
type="hidden",
name="passwordResetToken",
value=token
)
input(name='login_after', type='hidden', value="true")
.alert.alert-danger(ng-show="activationForm.response.error")
| #{translate("activation_token_expired")}
.form-group
label(for='email') #{translate("email")}
input.form-control(
type='email',
name='email',
placeholder="email@example.com"
required,
ng-model="email",
ng-init="email = #{JSON.stringify(email)}",
ng-model-options="{ updateOn: 'blur' }",
disabled
)
.form-group
label(for='password') #{translate("password")}
input.form-control#passwordField(
type='password',
name='password',
placeholder="********",
required,
ng-model="password",
complex-password,
focus="true"
)
span.small.text-primary(ng-show="activationForm.password.$error.complexPassword", ng-bind-html="complexPasswordErrorMessage")
.actions
button.btn-primary.btn(
type='submit'
ng-disabled="activationForm.inflight || activationForm.password.$error.required|| activationForm.password.$error.complexPassword"
)
span(ng-show="!activationForm.inflight") #{translate("activate")}
span(ng-show="activationForm.inflight") #{translate("activating")}...
script(type='text/javascript').
window.passwordStrengthOptions = !{JSON.stringify(settings.passwordStrengthOptions || {})}

View file

@ -20,6 +20,7 @@ block content
placeholder='email@example.com',
ng-model="email",
ng-model-options="{ updateOn: 'blur' }",
ng-init="email = #{JSON.stringify(email)}",
focus="true"
)
span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")

View file

@ -16,10 +16,11 @@ block content
ng-cloak
)
input(type="hidden", name="_csrf", value=csrfToken)
form-messages(for="passwordResetForm")
.alert.alert-success(ng-show="passwordResetForm.response.success")
| #{translate("password_has_been_reset")}.
a(href='/login') #{translate("login_here")}
.alert.alert-success(ng-show="passwordResetForm.response.success")
| #{translate("password_has_been_reset")}.
a(href='/login') #{translate("login_here")}
.alert.alert-danger(ng-show="passwordResetForm.response.error")
| #{translate("password_reset_token_expired")}
.form-group
input.form-control#passwordField(

View file

@ -106,6 +106,10 @@ module.exports =
url: "http://localhost:3036"
sixpack:
url: ""
references:
url: "http://localhost:3040"
notifications:
url: "http://localhost:3042"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@ -125,6 +129,9 @@ module.exports =
# Same, but with http auth credentials.
httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@localhost:3000'
maxEntitiesPerProject: 2000
# Security
# --------
security:
@ -143,6 +150,8 @@ module.exports =
versioning: true
compileTimeout: 60
compileGroup: "standard"
references: true
templates: true
plans: plans = [{
planCode: "personal"
@ -291,7 +300,7 @@ module.exports =
title: "ShareLaTeX Community Edition"
left_footer: [{
text: "Powered by <a href='https://www.sharelatex.com'>ShareLaTeX</a> © 2015"
text: "Powered by <a href='https://www.sharelatex.com'>ShareLaTeX</a> © 2016"
}]
right_footer: [{

View file

@ -27,18 +27,23 @@
"http-proxy": "^1.8.1",
"jade": "~1.3.1",
"ldapjs": "^0.7.1",
<<<<<<< HEAD
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
=======
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1",
>>>>>>> master
"lynx": "0.1.1",
"marked": "^0.3.3",
"method-override": "^2.3.3",
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.2.0",
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
"mimelib": "0.2.14",
"mocha": "1.17.1",
"mongojs": "0.18.2",
"mongoose": "4.1.0",
"multer": "^0.1.8",
"node-uuid": "1.4.1",
"nodemailer": "0.6.1",
"nodemailer": "2.1.0",
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
"redback": "0.4.0",
"redis": "0.10.1",

View file

@ -16,7 +16,11 @@ define [
onUploadCallback: "="
onValidateBatch: "="
onErrorCallback: "="
onSubmitCallback: "="
onCancelCallback: "="
autoUpload: "="
params: "="
control: "="
}
link: (scope, element, attrs) ->
multiple = scope.multiple or false
@ -32,19 +36,27 @@ define [
uploadButton: scope.uploadButtonText or "Upload"
dragAreaText = scope.dragAreaText or "drag here"
hintText = scope.hintText or ""
maxConnections = scope.maxConnections or 1
onComplete = scope.onCompleteCallback or () ->
onUpload = scope.onUploadCallback or () ->
onError = scope.onErrorCallback or () ->
onValidateBatch = scope.onValidateBatch or () ->
onSubmit = scope.onSubmitCallback or () ->
onCancel = scope.onCancelCallback or () ->
if !scope.autoUpload?
autoUpload = true
else
autoUpload = scope.autoUpload
params = scope.params or {}
params._csrf = window.csrfToken
q = new qq.FineUploader
element: element[0]
multiple: multiple
autoUpload: autoUpload
disabledCancelForFormUploads: true
validation: validation
maxConnections: maxConnections
request:
endpoint: endpoint
forceMultipart: true
@ -55,6 +67,8 @@ define [
onUpload: onUpload
onValidateBatch: onValidateBatch
onError: onError
onSubmit: onSubmit
onCancel: onCancel
text: text
template: """
<div class="qq-uploader">
@ -69,5 +83,7 @@ define [
<ul class="qq-upload-list"></ul>
</div>
"""
window.q = q
scope.control?.q = q
return q
}

View file

@ -8,6 +8,7 @@ define [
"ide/permissions/PermissionsManager"
"ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
"ide/settings/index"
"ide/share/index"
"ide/chat/index"
@ -24,6 +25,7 @@ define [
"directives/onEnter"
"directives/stopPropagation"
"directives/rightClick"
"services/queued-http"
"filters/formatDate"
"main/event"
"main/account-upgrade"
@ -37,6 +39,7 @@ define [
PermissionsManager
PdfManager
BinaryFilesManager
ReferencesManager
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
@ -72,6 +75,7 @@ define [
ide.project_id = $scope.project_id = window.project_id
ide.$scope = $scope
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
ide.connectionManager = new ConnectionManager(ide, $scope)
ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope)

View file

@ -37,8 +37,12 @@ define [
# Restore previously recorded state
if (state = ide.localStorage("layout.#{name}"))?
options.west = state.west
options.east = state.east
if state.east?
if !attrs.minimumRestoreSizeEast? or (state.east.size >= attrs.minimumRestoreSizeEast and !state.east.initClosed)
options.east = state.east
if state.west?
if !attrs.minimumRestoreSizeWest? or (state.west.size >= attrs.minimumRestoreSizeWest and !state.west.initClosed)
options.west = state.west
repositionControls = () ->
state = element.layout().readState()

View file

@ -36,6 +36,7 @@ define [
@doc?.detachFromAce()
editorDoc = @ace?.getSession().getDocument()
editorDoc?.off "change", @_checkConsistency
@ide.$scope.$emit 'document:closed', @doc
_checkConsistency: () ->
# We've been seeing a lot of errors when I think there shouldn't be
@ -116,6 +117,26 @@ define [
flush: () ->
@doc?.flushPendingOps()
chaosMonkey: (line = 0, char = "a") ->
orig = char
copy = null
pos = 0
timer = () =>
unless copy? and copy.length
copy = orig.slice() + ' ' + new Date() + '\n'
line += if Math.random() > 0.1 then 1 else -2
line = 0 if line < 0
pos = 0
char = copy[0]
copy = copy.slice(1)
@ace.session.insert({row: line, column: pos}, char)
pos += 1
@_cm = setTimeout timer, 100 + if Math.random() < 0.1 then 1000 else 0
@_cm = timer()
clearChaosMonkey: () ->
clearTimeout @_cm
pollSavedStatus: () ->
# returns false if doc has ops waiting to be acknowledged or
# sent that haven't changed since the last time we checked.

View file

@ -86,6 +86,7 @@ define [
@_bindToDocumentEvents(doc, new_sharejs_doc)
callback null, new_sharejs_doc
_bindToDocumentEvents: (doc, sharejs_doc) ->
sharejs_doc.on "error", (error, meta) =>
if error?.message?.match "maxDocLength"
@ -98,7 +99,7 @@ define [
@ide.reportError(error, meta)
@ide.showGenericMessageModal(
"Out of sync"
"Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
"Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a href='http://sharelatex.tenderapp.com/help/kb/browsers/editor-out-of-sync-problems'>Please see this help guide for more information</a>"
)
@openDoc(doc, forceReopen: true)

View file

@ -18,7 +18,7 @@ define [
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage) ->
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory) ->
monkeyPatchSearch($rootScope, $compile)
return {
@ -29,6 +29,7 @@ define [
fontSize: "="
autoComplete: "="
sharejsDoc: "="
spellCheck: "="
spellCheckLanguage: "="
highlights: "="
text: "="
@ -55,7 +56,9 @@ define [
scope.name = attrs.aceEditor
autoCompleteManager = new AutoCompleteManager(scope, editor, element)
spellCheckManager = new SpellCheckManager(scope, editor, element)
if scope.spellCheck # only enable spellcheck when explicitly required
spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache)
undoManager = new UndoManager(scope, editor, element)
highlightsManager = new HighlightsManager(scope, editor, element)
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
@ -70,6 +73,21 @@ define [
editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall"
# For European keyboards, the / is above 7 so needs Shift pressing.
# This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment.
# This doesn't do anything for LaTeX, so remap this to togglecomment to
# work for European keyboards as normal.
editor.commands.removeCommand "toggleBlockComment"
editor.commands.removeCommand "togglecomment"
editor.commands.addCommand {
name: "togglecomment",
bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" },
exec: (editor) -> editor.toggleCommentLines(),
multiSelectAction: "forEachLine",
scrollIntoView: "selectionPart"
}
# Trigger search AND replace on CMD+F
editor.commands.addCommand
name: "find",
@ -77,7 +95,6 @@ define [
exec: (editor) ->
ace.require("ace/ext/searchbox").Search(editor, true)
readOnly: true
editor.commands.removeCommand "replace"
# Bold text on CMD+B
editor.commands.addCommand

View file

@ -41,7 +41,43 @@ define [
SnippetCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
callback null, Snippets
@editor.completers = [@suggestionManager, SnippetCompleter]
references = @$scope.$root._references
ReferencesCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
range = new Range(pos.row, 0, pos.row, pos.column)
lineUpToCursor = editor.getSession().getTextRange(range)
commandFragment = getLastCommandFragment(lineUpToCursor)
if commandFragment
citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?){(.*,)?(\w*)/)
if citeMatch
commandName = citeMatch[1]
previousArgs = citeMatch[2]
currentArg = citeMatch[3]
if previousArgs == undefined
previousArgs = ""
previousArgsCaption = if previousArgs.length > 8 then "…," else previousArgs
result = []
result.push {
caption: "\\#{commandName}{",
snippet: "\\#{commandName}{",
meta: "reference",
score: 11000
}
if references.keys and references.keys.length > 0
references.keys.forEach (key) ->
if !(key in [null, undefined])
result.push({
caption: "\\#{commandName}{#{previousArgsCaption}#{key}",
value: "\\#{commandName}{#{previousArgs}#{key}",
meta: "reference",
score: 10000
})
callback null, result
else
callback null, result
@editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter]
disable: () ->
@editor.setOptions({

View file

@ -14,7 +14,7 @@ define () ->
caption: "\\begin{#{env}}..."
snippet: """
\\begin{#{env}}
$1
\t$1
\\end{#{env}}
"""
meta: "env"
@ -24,8 +24,8 @@ define () ->
caption: "\\begin{array}..."
snippet: """
\\begin{array}{${1:cc}}
$2 & $3 \\\\\\\\
$4 & $5
\t$2 & $3 \\\\\\\\
\t$4 & $5
\\end{array}
"""
meta: "env"
@ -33,10 +33,10 @@ define () ->
caption: "\\begin{figure}..."
snippet: """
\\begin{figure}
\\centering
\\includegraphics{$1}
\\caption{${2:Caption}}
\\label{${3:fig:my_label}}
\t\\centering
\t\\includegraphics{$1}
\t\\caption{${2:Caption}}
\t\\label{${3:fig:my_label}}
\\end{figure}
"""
meta: "env"
@ -44,8 +44,8 @@ define () ->
caption: "\\begin{tabular}..."
snippet: """
\\begin{tabular}{${1:c|c}}
$2 & $3 \\\\\\\\
$4 & $5
\t$2 & $3 \\\\\\\\
\t$4 & $5
\\end{tabular}
"""
meta: "env"
@ -53,13 +53,13 @@ define () ->
caption: "\\begin{table}..."
snippet: """
\\begin{table}[$1]
\\centering
\\begin{tabular}{${2:c|c}}
$3 & $4 \\\\\\\\
$5 & $6
\\end{tabular}
\\caption{${7:Caption}}
\\label{${8:tab:my_label}}
\t\\centering
\t\\begin{tabular}{${2:c|c}}
\t\t$3 & $4 \\\\\\\\
\t\t$5 & $6
\t\\end{tabular}
\t\\caption{${7:Caption}}
\t\\label{${8:tab:my_label}}
\\end{table}
"""
meta: "env"
@ -67,7 +67,7 @@ define () ->
caption: "\\begin{list}..."
snippet: """
\\begin{list}
\\item $1
\t\\item $1
\\end{list}
"""
meta: "env"
@ -75,7 +75,7 @@ define () ->
caption: "\\begin{enumerate}..."
snippet: """
\\begin{enumerate}
\\item $1
\t\\item $1
\\end{enumerate}
"""
meta: "env"
@ -83,7 +83,7 @@ define () ->
caption: "\\begin{itemize}..."
snippet: """
\\begin{itemize}
\\item $1
\t\\item $1
\\end{itemize}
"""
meta: "env"
@ -91,7 +91,7 @@ define () ->
caption: "\\begin{frame}..."
snippet: """
\\begin{frame}{${1:Frame Title}}
$2
\t$2
\\end{frame}
"""
meta: "env"

View file

@ -5,7 +5,7 @@ define [
Range = ace.require("ace/range").Range
class SpellCheckManager
constructor: (@$scope, @editor, @element) ->
constructor: (@$scope, @editor, @element, @cache) ->
$(document.body).append @element.find(".spell-check-menu")
@updatedLines = []
@ -102,6 +102,8 @@ define [
learnWord: (highlight) ->
@apiRequest "/learn", word: highlight.word
@highlightedWordManager.removeWord highlight.word
language = @$scope.spellCheckLanguage
@cache?.put("#{language}:#{highlight.word}", true)
getHighlightedWordAtCursor: () ->
cursor = @editor.getCursorPosition()
@ -143,24 +145,67 @@ define [
runSpellCheck: (linesToProcess) ->
{words, positions} = @getWords(linesToProcess)
language = @$scope.spellCheckLanguage
@apiRequest "/check", {language: language, words: words}, (error, result) =>
if error? or !result? or !result.misspellings?
return null
highlights = []
seen = {}
newWords = []
newPositions = []
# iterate through all words, building up a list of
# newWords/newPositions not in the cache
for word, i in words
key = "#{language}:#{word}"
seen[key] ?= @cache.get(key) # avoid hitting the cache unnecessarily
cached = seen[key]
if not cached?
newWords.push words[i]
newPositions.push positions[i]
else if cached is true
# word is correct
else
highlights.push
column: positions[i].column
row: positions[i].row
word: word
suggestions: cached
words = newWords
positions = newPositions
displayResult = (highlights) =>
if linesToProcess?
for shouldProcess, row in linesToProcess
@highlightedWordManager.clearRows(row, row) if shouldProcess
else
@highlightedWordManager.clearRows()
for highlight in highlights
@highlightedWordManager.addHighlight highlight
for misspelling in result.misspellings
word = words[misspelling.index]
position = positions[misspelling.index]
@highlightedWordManager.addHighlight
column: position.column
row: position.row
word: word
suggestions: misspelling.suggestions
if not words.length
displayResult highlights
else
@apiRequest "/check", {language: language, words: words}, (error, result) =>
if error? or !result? or !result.misspellings?
return null
mispelled = []
for misspelling in result.misspellings
word = words[misspelling.index]
position = positions[misspelling.index]
mispelled[misspelling.index] = true
highlights.push
column: position.column
row: position.row
word: word
suggestions: misspelling.suggestions
key = "#{language}:#{word}"
if not seen[key]
@cache.put key, misspelling.suggestions
seen[key] = true
for word, i in words when not mispelled[i]
key = "#{language}:#{word}"
if not seen[key]
@cache.put(key, true)
seen[key] = true
displayResult highlights
getWords: (linesToProcess) ->
lines = @editor.getValue().split("\n")

View file

@ -20,6 +20,12 @@ define [
@_bindToSocketEvents()
@$scope.multiSelectedCount = 0
$(document).on "click", =>
@clearMultiSelectedEntities()
$scope.$digest()
_bindToSocketEvents: () ->
@ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
parent_folder = @findEntityById(parent_folder_id) or @$scope.rootFolder
@ -65,6 +71,7 @@ define [
@$scope.$apply () =>
@_deleteEntityFromScope entity
@recalculateDocList()
@$scope.$emit "entity:deleted", entity
@ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
entity = @findEntityById(entity_id)
@ -79,6 +86,61 @@ define [
entity.selected = false
entity.selected = true
toggleMultiSelectEntity: (entity) ->
entity.multiSelected = !entity.multiSelected
@$scope.multiSelectedCount = @multiSelectedCount()
multiSelectedCount: () ->
count = 0
@forEachEntity (entity) ->
if entity.multiSelected
count++
return count
getMultiSelectedEntities: () ->
entities = []
@forEachEntity (e) ->
if e.multiSelected
entities.push e
return entities
getMultiSelectedEntityChildNodes: () ->
entities = @getMultiSelectedEntities()
paths = {}
for entity in entities
paths[@getEntityPath(entity)] = entity
prefixes = {}
for path, entity of paths
parts = path.split("/")
if parts.length <= 1
continue
else
# Record prefixes a/b/c.tex -> 'a' and 'a/b'
for i in [1..(parts.length - 1)]
prefixes[parts.slice(0,i).join("/")] = true
child_entities = []
for path, entity of paths
# If the path is in the prefixes, then it's a parent folder and
# should be ignore
if !prefixes[path]?
child_entities.push entity
return child_entities
clearMultiSelectedEntities: () ->
return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click'
@forEachEntity (entity) ->
entity.multiSelected = false
@$scope.multiSelectedCount = 0
multiSelectSelectedEntity: () ->
@findSelectedEntity()?.multiSelected = true
existsInFolder: (folder_id, name) ->
folder = @findEntityById(folder_id)
return false if !folder?
entity = @_findEntityByPathInFolder(folder, name)
return entity?
findSelectedEntity: () ->
selected = null
@forEachEntity (entity) ->
@ -277,7 +339,7 @@ define [
deleteEntity: (entity, callback = (error) ->) ->
# We'll wait for the socket.io notification to
# delete from scope.
return @ide.$http {
return @ide.queuedHttp {
method: "DELETE"
url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}"
headers:
@ -289,7 +351,7 @@ define [
# since that would break the tree structure.
return if @_isChildFolder(entity, parent_folder)
@_moveEntityInScope(entity, parent_folder)
return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", {
return @ide.queuedHttp.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", {
folder_id: parent_folder.id
_csrf: window.csrfToken
}
@ -316,8 +378,6 @@ define [
entity.deleted = true
@$scope.deletedDocs.push entity
@$scope.$emit "entity:deleted", entity
_moveEntityInScope: (entity, parent_folder) ->
return if entity in parent_folder.children
@_deleteEntityFromScope(entity, moveToDeleted: false)

View file

@ -61,6 +61,8 @@ define [
$scope.state.inflight = true
ide.fileTreeManager
.createDoc(name, parent_folder)
.error (e)->
$scope.error = e
.success () ->
$scope.state.inflight = false
$modalInstance.close()
@ -90,6 +92,8 @@ define [
$scope.state.inflight = true
ide.fileTreeManager
.createFolder(name, parent_folder)
.error (e)->
$scope.error = e
.success () ->
$scope.state.inflight = false
$modalInstance.close()
@ -99,17 +103,30 @@ define [
]
App.controller "UploadFileModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window"
($scope, ide, $modalInstance, $timeout, parent_folder, $window) ->
$scope.parent_folder_id = parent_folder?.id
$scope.tooManyFiles = false
$scope.rateLimitHit = false
$scope.secondsToRedirect = 10
$scope.notLoggedIn = false
$scope.conflicts = []
$scope.control = {}
uploadCount = 0
$scope.onUpload = () ->
uploadCount++
needToLogBackIn = ->
$scope.notLoggedIn = true
decreseTimeout = ->
$timeout (() ->
if $scope.secondsToRedirect == 0
$window.location.href = "/login?redir=/project/#{ide.project_id}"
else
decreseTimeout()
$scope.secondsToRedirect = $scope.secondsToRedirect - 1
), 1000
$scope.max_files = 20
decreseTimeout()
$scope.max_files = 40
$scope.onComplete = (error, name, response) ->
$timeout (() ->
uploadCount--
@ -127,8 +144,40 @@ define [
return true
$scope.onError = (id, name, reason)->
console.log(id, name, reason)
if reason.indexOf("429") != -1
$scope.rateLimitHit = true
else if reason.indexOf("403") != -1
needToLogBackIn()
_uploadTimer = null
uploadIfNoConflicts = () ->
if $scope.conflicts.length == 0
$scope.doUpload()
uploadCount = 0
$scope.onSubmit = (id, name) ->
uploadCount++
if ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)
$scope.conflicts.push name
$scope.$apply()
if !_uploadTimer?
_uploadTimer = setTimeout () ->
_uploadTimer = null
uploadIfNoConflicts()
, 0
return true
$scope.onCancel = (id, name) ->
uploadCount--
index = $scope.conflicts.indexOf(name)
if index > -1
$scope.conflicts.splice(index, 1)
$scope.$apply()
uploadIfNoConflicts()
$scope.doUpload = () ->
$scope.control?.q?.uploadStoredFiles()
$scope.cancel = () ->
$modalInstance.dismiss('cancel')

View file

@ -2,9 +2,23 @@ define [
"base"
], (App) ->
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
$scope.select = () ->
ide.fileTreeManager.selectEntity($scope.entity)
$scope.$emit "entity:selected", $scope.entity
$scope.select = (e) ->
if e.ctrlKey or e.metaKey
e.stopPropagation()
initialMultiSelectCount = ide.fileTreeManager.multiSelectedCount()
ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) == 0
if initialMultiSelectCount == 0
# On first multi selection, also include the current active/open file.
ide.fileTreeManager.multiSelectSelectedEntity()
else
ide.fileTreeManager.selectEntity($scope.entity)
$scope.$emit "entity:selected", $scope.entity
$scope.draggableHelper = () ->
if ide.fileTreeManager.multiSelectedCount() > 0
return $("<strong style='z-index:100'>#{ide.fileTreeManager.multiSelectedCount()} Files</strong>")
else
return $("<strong style='z-index:100'>#{$scope.entity.name}</strong>")
$scope.inputs =
name: $scope.entity.name
@ -24,10 +38,15 @@ define [
$scope.startRenaming() if $scope.entity.selected
$scope.openDeleteModal = () ->
if ide.fileTreeManager.multiSelectedCount() > 0
entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
else
entities = [$scope.entity]
$modal.open(
templateUrl: "deleteEntityModalTemplate"
controller: "DeleteEntityModalController"
scope: $scope
resolve:
entities: () -> entities
)
$scope.$on "delete:selected", () ->
@ -35,18 +54,18 @@ define [
]
App.controller "DeleteEntityModalController", [
"$scope", "ide", "$modalInstance",
($scope, ide, $modalInstance) ->
"$scope", "ide", "$modalInstance", "entities"
($scope, ide, $modalInstance, entities) ->
$scope.state =
inflight: false
$scope.entities = entities
$scope.delete = () ->
$scope.state.inflight = true
ide.fileTreeManager
.deleteEntity($scope.entity)
.success () ->
$scope.state.inflight = false
$modalInstance.close()
for entity in $scope.entities
ide.fileTreeManager.deleteEntity(entity)
$modalInstance.close()
$scope.cancel = () ->
$modalInstance.dismiss('cancel')

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