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: paths:
"moment": "libs/moment-2.9.0" "moment": "libs/moment-2.9.0"
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML" "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: shim:
"libs/pdf": "libs/pdf":
deps: ["libs/pdfjs-1.0.1040/compatibility"] deps: ["libs/pdfjs-1.3.91/compatibility"]
skipDirOptimize: true skipDirOptimize: true
modules: [ modules: [

View file

@ -3,8 +3,12 @@ logger = require 'logger-sharelatex'
logger.initialize("web-sharelatex") logger.initialize("web-sharelatex")
logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user
logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project
if Settings.sentry?.dsn?
logger.initializeErrorReporting(Settings.sentry.dsn)
metrics = require("metrics-sharelatex") metrics = require("metrics-sharelatex")
metrics.initialize("web") metrics.initialize("web")
metrics.memory.monitor(logger)
Server = require("./app/js/infrastructure/Server") Server = require("./app/js/infrastructure/Server")
Errors = require "./app/js/errors" Errors = require "./app/js/errors"
@ -15,6 +19,10 @@ argv = require("optimist")
.argv .argv
Server.app.use (error, req, res, next) -> 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" 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 res.statusCode = error.status or 500
if res.statusCode == 500 if res.statusCode == 500

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,11 +67,16 @@ module.exports = EditorHttpController =
project_id = req.params.Project_id project_id = req.params.Project_id
name = req.body.name name = req.body.name
parent_folder_id = req.body.parent_folder_id 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) if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400 return res.sendStatus 400
EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) -> EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) ->
return next(error) if error? if error == "project_has_to_many_files"
res.json doc 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) -> addFolder: (req, res, next) ->
project_id = req.params.Project_id project_id = req.params.Project_id
@ -80,8 +85,12 @@ module.exports = EditorHttpController =
if !EditorHttpController._nameIsAcceptableLength(name) if !EditorHttpController._nameIsAcceptableLength(name)
return res.sendStatus 400 return res.sendStatus 400
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) -> EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
return next(error) if error? if error == "project_has_to_many_files"
res.json doc 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) -> renameEntity: (req, res, next) ->
project_id = req.params.Project_id project_id = req.params.Project_id

View file

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

View file

@ -6,24 +6,32 @@ settings = require("settings-sharelatex")
oneMinInMs = 60 * 1000 oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5 fiveMinsInMs = oneMinInMs * 5
module.exports = module.exports = FileStoreHandler =
uploadFileFromDisk: (project_id, file_id, fsPath, callback)-> uploadFileFromDisk: (project_id, file_id, fsPath, callback)->
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk" fs.lstat fsPath, (err, stat)->
readStream = fs.createReadStream(fsPath) if err?
opts = logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file"
method: "post" callback(err)
uri: @_buildUrl(project_id, file_id) if !stat.isFile()
timeout:fiveMinsInMs logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining"
writeStream = request(opts) return callback(new Error("can not upload symlink"))
readStream.pipe writeStream
writeStream.on "end", callback logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
readStream.on "error", (err)-> readStream = fs.createReadStream(fsPath)
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk" opts =
callback err method: "post"
writeStream.on "error", (err)-> uri: FileStoreHandler._buildUrl(project_id, file_id)
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk" timeout:fiveMinsInMs
callback err 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)-> 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" 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") PasswordResetHandler = require("./PasswordResetHandler")
RateLimiter = require("../../infrastructure/RateLimiter") RateLimiter = require("../../infrastructure/RateLimiter")
AuthenticationController = require("../Authentication/AuthenticationController")
UserGetter = require("../User/UserGetter")
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
module.exports = module.exports =
@ -37,14 +39,19 @@ module.exports =
title:"set_password" title:"set_password"
passwordResetToken: req.session.resetToken passwordResetToken: req.session.resetToken
setNewUserPassword: (req, res)-> setNewUserPassword: (req, res, next)->
{passwordResetToken, password} = req.body {passwordResetToken, password} = req.body
if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0
return res.sendStatus 400 return res.sendStatus 400
delete req.session.resetToken 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? return next(err) if err?
if found 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 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? return callback(error) if error?
callback null, true callback null, true
setNewUserPassword: (token, password, callback = (error, found) ->)-> setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)-> OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)->
if err then return callback(err) if err then return callback(err)
if !user_id? if !user_id?
return callback null, false return callback null, false, null
AuthenticationManager.setUserPassword user_id, password, (err) -> AuthenticationManager.setUserPassword user_id, password, (err) ->
if err then return callback(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 User = require('../../models/User').User
TagsHandler = require("../Tags/TagsHandler") TagsHandler = require("../Tags/TagsHandler")
SubscriptionLocator = require("../Subscription/SubscriptionLocator") SubscriptionLocator = require("../Subscription/SubscriptionLocator")
NotificationsHandler = require("../Notifications/NotificationsHandler")
LimitationsManager = require("../Subscription/LimitationsManager") LimitationsManager = require("../Subscription/LimitationsManager")
_ = require("underscore") _ = require("underscore")
Settings = require("settings-sharelatex") Settings = require("settings-sharelatex")
@ -51,7 +52,7 @@ module.exports = ProjectController =
deleteProject: (req, res) -> deleteProject: (req, res) ->
project_id = req.params.Project_id project_id = req.params.Project_id
forever = req.query?.forever? 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 if forever
doDelete = projectDeleter.deleteProject doDelete = projectDeleter.deleteProject
@ -125,6 +126,8 @@ module.exports = ProjectController =
async.parallel { async.parallel {
tags: (cb)-> tags: (cb)->
TagsHandler.getAllTags user_id, cb TagsHandler.getAllTags user_id, cb
notifications: (cb)->
NotificationsHandler.getUserNotifications user_id, cb
projects: (cb)-> projects: (cb)->
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)-> hasSubscription: (cb)->
@ -137,6 +140,9 @@ module.exports = ProjectController =
return next(err) return next(err)
logger.log results:results, user_id:user_id, "rendering project list" logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0] 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] projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
user = results.user user = results.user
ProjectController._injectProjectOwners projects, (error, projects) -> ProjectController._injectProjectOwners projects, (error, projects) ->
@ -147,6 +153,7 @@ module.exports = ProjectController =
priority_title: true priority_title: true
projects: projects projects: projects
tags: tags tags: tags
notifications: notifications or []
user: user user: user
hasSubscription: results.hasSubscription[0] hasSubscription: results.hasSubscription[0]
} }
@ -168,15 +175,15 @@ module.exports = ProjectController =
return res.render("general/closed", {title:"updating_site"}) return res.render("general/closed", {title:"updating_site"})
if req.session.user? if req.session.user?
user_id = req.session.user._id user_id = req.session.user._id
anonymous = false anonymous = false
else else
anonymous = true anonymous = true
user_id = 'openUser' user_id = 'openUser'
project_id = req.params.Project_id project_id = req.params.Project_id
logger.log project_id:project_id, "loading editor" logger.log project_id:project_id, "loading editor"
async.parallel { async.parallel {
project: (cb)-> project: (cb)->
Project.findPopulatedById project_id, cb Project.findPopulatedById project_id, cb
@ -193,7 +200,7 @@ module.exports = ProjectController =
SubscriptionLocator.getUsersSubscription user_id, cb SubscriptionLocator.getUsersSubscription user_id, cb
activate: (cb)-> activate: (cb)->
InactiveProjectManager.reactivateProjectIfRequired project_id, cb InactiveProjectManager.reactivateProjectIfRequired project_id, cb
markAsOpened: (cb)-> markAsOpened: (cb)->
#don't need to wait for this to complete #don't need to wait for this to complete
ProjectUpdateHandler.markAsOpened project_id, -> ProjectUpdateHandler.markAsOpened project_id, ->
cb() cb()
@ -205,6 +212,7 @@ module.exports = ProjectController =
user = results.user user = results.user
subscription = results.subscription subscription = results.subscription
daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000 daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000
logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor" 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? if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
allowedFreeTrial = !!subscription.freeTrial.allowed || true allowedFreeTrial = !!subscription.freeTrial.allowed || true
logger.log project_id:project_id, "rendering editor page" logger.log project_id:project_id, "rendering editor page"
res.render 'project/editor', res.render 'project/editor',
title: project.name title: project.name
@ -310,5 +319,4 @@ do generateThemeList = () ->
for file in files for file in files
if file.slice(-2) == "js" and file.match(/^theme-/) if file.slice(-2) == "js" and file.match(/^theme-/)
cleanName = file.slice(0,-3).slice(6) cleanName = file.slice(0,-3).slice(6)
THEME_LIST.push cleanName THEME_LIST.push cleanName

View file

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

View file

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

View file

@ -8,7 +8,7 @@ _ = require("underscore")
module.exports = module.exports =
getDetails: (project_id, callback)-> 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? if err?
logger.err err:err, project_id:project_id, "error getting project" logger.err err:err, project_id:project_id, "error getting project"
return callback(err) return callback(err)
@ -37,7 +37,7 @@ module.exports =
renameProject: (project_id, newName, callback = ->)-> renameProject: (project_id, newName, callback = ->)->
logger.log project_id: project_id, newName:newName, "renaming project" 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? if err? or !project?
logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename" logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
return callback(err) return callback(err)

View file

@ -4,61 +4,86 @@ projectLocator = require('./ProjectLocator')
projectOptionsHandler = require('./ProjectOptionsHandler') projectOptionsHandler = require('./ProjectOptionsHandler')
DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler") DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler")
DocstoreManager = require "../Docstore/DocstoreManager" DocstoreManager = require "../Docstore/DocstoreManager"
Project = require("../../models/Project").Project ProjectGetter = require("./ProjectGetter")
_ = require('underscore') _ = require('underscore')
async = require('async') 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 = {} module.exports = ProjectDuplicator =
for docContent in docContentsArray
docContents[docContent._id] = docContent
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)-> jobs = originalFolder.docs.map (doc)->
projectEntityHandler.setRootDoc newProject, doc_id 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)-> async.series jobs, 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
copyFiles = (originalFolder, newParentFolder, callback)-> _copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
jobs = originalFolder.fileRefs.map (file)-> jobs = originalFolder.fileRefs.map (file)->
return (callback)-> return (cb)->
projectEntityHandler.copyFileFromExistingProject newProject, newParentFolder._id, originalProject._id, file, callback projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
async.parallelLimit jobs, 5, callback 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], -> jobs = originalFolder.folders.map (childFolder)->
callback(err, newProject) 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 dropbox:false
compileTimeout: 60 compileTimeout: 60
compileGroup:"standard" compileGroup:"standard"
templates: false
references: false
if project.owner_ref.features? if project.owner_ref.features?
if project.owner_ref.features.collaborators? if project.owner_ref.features.collaborators?
@ -37,7 +39,11 @@ module.exports = ProjectEditorHandler =
if project.owner_ref.features.compileTimeout? if project.owner_ref.features.compileTimeout?
result.features.compileTimeout = project.owner_ref.features.compileTimeout result.features.compileTimeout = project.owner_ref.features.compileTimeout
if project.owner_ref.features.compileGroup? if project.owner_ref.features.compileGroup?
result.features.compileGroup = 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" result.owner = @buildUserModelView project.owner_ref, "owner"

View file

@ -1,4 +1,5 @@
Project = require('../../models/Project').Project Project = require('../../models/Project').Project
settings = require "settings-sharelatex"
Doc = require('../../models/Doc').Doc Doc = require('../../models/Doc').Doc
Folder = require('../../models/Folder').Folder Folder = require('../../models/Folder').Folder
File = require('../../models/File').File File = require('../../models/File').File
@ -75,23 +76,21 @@ module.exports = ProjectEntityHandler =
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
documentUpdaterHandler.flushProjectToMongo project_id, (error) -> documentUpdaterHandler.flushProjectToMongo project_id, (error) ->
return callback(error) if 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? return callback(error) if error?
requests = [] requests = []
self.getAllDocs project_id, (error, docs) -> self.getAllDocs project_id, (error, docs) ->
return callback(error) if error? return callback(error) if error?
for docPath, doc of docs for docPath, doc of docs
do (docPath, doc) -> do (docPath, doc) ->
requests.push (callback) -> requests.push (cb) ->
tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb
callback
self.getAllFiles project_id, (error, files) -> self.getAllFiles project_id, (error, files) ->
return callback(error) if error? return callback(error) if error?
for filePath, file of files for filePath, file of files
do (filePath, file) -> do (filePath, file) ->
requests.push (callback) -> requests.push (cb) ->
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb
callback
async.series requests, (err) -> async.series requests, (err) ->
logger.log project_id:project_id, "finished flushing project to tpds" logger.log project_id:project_id, "finished flushing project to tpds"
callback(err) callback(err)
@ -110,27 +109,36 @@ module.exports = ProjectEntityHandler =
options = {} options = {}
DocstoreManager.getDoc project_id, doc_id, options, callback 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) -> addDoc: (project_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
logger.log project: project._id, folder_id: folder_id, doc_name: docName, "adding doc" ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
return callback(err) if err? if err?
confirmFolder project, folder_id, (folder_id)=> logger.err project_id:project_id, err:err, "error getting project for add doc"
doc = new Doc name: docName return callback(err)
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, callback
# which hasn't been created in docstore.
DocstoreManager.updateDoc project._id.toString(), doc._id.toString(), docLines, (err, modified, rev) -> addDocWithProject: (project, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
return callback(err) if err? project_id = project._id
Project.putElement project._id, folder_id, doc, "doc", (err, result)=> logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
return callback(err) if err? confirmFolder project, folder_id, (folder_id)=>
tpdsUpdateSender.addDoc { doc = new Doc name: docName
project_id: project._id, # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
doc_id: doc._id # which hasn't been created in docstore.
path: result.path.fileSystem, DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
project_name: project.name, return callback(err) if err?
rev: 0
}, (err) -> ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
return callback(err) if 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) ->) -> 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 # 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? return callback(error) if error?
ProjectEntityHandler.addDoc project_id, null, name, lines, callback ProjectEntityHandler.addDoc project_id, null, name, lines, callback
addFile: (project_or_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)-> addFile: (project_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
ProjectGetter.getProjectWithOnlyFolders project_or_id, (err, project) -> ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file" if err?
return callback(err) if err? logger.err project_id:project_id, err:err, "error getting project for add file"
confirmFolder project, folder_id, (folder_id)-> return callback(err)
fileRef = new File name : fileName ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, callback
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)
replaceFile: (project_or_id, file_id, fsPath, callback)-> addFileWithProject: (project, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
Project.getProject project_or_id, "", (err, project) -> 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? return callback(err) if err?
findOpts = findOpts =
project_id:project._id project_id:project._id
@ -182,21 +202,36 @@ module.exports = ProjectEntityHandler =
Project.update conditons, update, {}, (err, second)-> Project.update conditons, update, {}, (err, second)->
callback() callback()
copyFileFromExistingProject: (project_or_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)-> copyFileFromExistingProject: (project_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"
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) ->
return callback(err) if err? if err?
confirmFolder project, folder_id, (folder_id)=> logger.err project_id:project_id, err:err, "error getting project for copy file from existing project"
if !origonalFileRef? return callback(err)
logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null" ProjectEntityHandler.copyFileFromExistingProjectWithProject project, folder_id, originalProject_id, origonalFileRef, callback
return callback()
fileRef = new File name : origonalFileRef.name
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)-> 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? 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" logger.err err:err, project_id:project._id, folder_id:folder_id, "error putting element as part of copy"
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=> return callback(err)
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, (error) -> tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
callback(error, fileRef, folder_id) 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)->)-> mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
self = @ self = @
@ -204,7 +239,7 @@ module.exports = ProjectEntityHandler =
folders = _.select folders, (folder)-> folders = _.select folders, (folder)->
return folder.length != 0 return folder.length != 0
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)=> ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
if path == '/' if path == '/'
logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder" 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]) return callback(null, [], project.rootFolder[0])
@ -217,7 +252,7 @@ module.exports = ProjectEntityHandler =
if parentFolder? if parentFolder?
parentFolder_id = parentFolder._id parentFolder_id = parentFolder._id
builtUpPath = "#{builtUpPath}/#{folderName}" builtUpPath = "#{builtUpPath}/#{folderName}"
projectLocator.findElementByPath project_id, builtUpPath, (err, foundFolder)=> projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=>
if !foundFolder? if !foundFolder?
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
@ -235,16 +270,23 @@ module.exports = ProjectEntityHandler =
folders = _.select folders, (folder)-> folders = _.select folders, (folder)->
!folder.filterOut !folder.filterOut
callback(null, folders, lastFolder) callback(null, folders, lastFolder)
addFolder: (project_or_id, parentFolder_id, folderName, callback) -> addFolder: (project_id, parentFolder_id, folderName, callback) ->
folder = new Folder name: folderName ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
Project.getProject project_or_id, "", (err, project) -> if err?
return callback(err) if err? logger.err project_id:project_id, err:err, "error getting project for add folder"
confirmFolder project, parentFolder_id, (parentFolder_id)=> return callback(err)
logger.log project: project_or_id, parentFolder_id:parentFolder_id, folderName:folderName, "new folder added" ProjectEntityHandler.addFolderWithProject project, parentFolder_id, folderName, callback
Project.putElement project._id, parentFolder_id, folder, "folder", (err, result)=>
if callback? addFolderWithProject: (project, parentFolder_id, folderName, callback = (err, folder, parentFolder_id)->) ->
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) ->)-> updateDocLines : (project_id, doc_id, lines, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> 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 logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set") return callback("No entityType set")
entityType = entityType.toLowerCase() entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=> ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
return callback(err) if err? return callback(err) if err?
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)-> projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)->
return callback(err) if err? return callback(err) if err?
@ -302,7 +344,7 @@ module.exports = ProjectEntityHandler =
return callback(error) if error? return callback(error) if error?
self._removeElementFromMongoArray Project, project_id, path.mongo, (err)-> self._removeElementFromMongoArray Project, project_id, path.mongo, (err)->
return callback(err) if 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? return callback(err) if err?
opts = opts =
project_id:project_id 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 logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set") return callback("No entityType set")
entityType = entityType.toLowerCase() entityType = entityType.toLowerCase()
Project.findById project_id, (err, project)=> ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (err, project)=>
return callback(error) if error? return callback(error) if error?
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
return callback(error) if error? 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 logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
return callback("No entityType set") return callback("No entityType set")
entityType = entityType.toLowerCase() 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)=> projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=>
if err? if err?
return callback err return callback err
@ -426,6 +468,72 @@ module.exports = ProjectEntityHandler =
} }
}, {}, callback }, {}, 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)-> confirmFolder = (project, folder_id, callback)->
logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project" logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project"
if folder_id+'' == 'undefined' if folder_id+'' == 'undefined'

View file

@ -2,31 +2,54 @@ mongojs = require("../../infrastructure/mongojs")
db = mongojs.db db = mongojs.db
ObjectId = mongojs.ObjectId ObjectId = mongojs.ObjectId
async = require "async" async = require "async"
Errors = require("../../errors")
logger = require("logger-sharelatex")
module.exports = ProjectGetter = module.exports = ProjectGetter =
EXCLUDE_DEPTH: 8 EXCLUDE_DEPTH: 8
getProjectWithoutDocLines: (project_id, callback=(error, project) ->) -> getProjectWithoutDocLines: (project_id, callback=(error, project) ->) ->
excludes = {} excludes = {}
for i in [1..@EXCLUDE_DEPTH] for i in [1..ProjectGetter.EXCLUDE_DEPTH]
excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0 excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) -> db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
callback error, projects[0] callback error, projects[0]
getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) -> getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) ->
excludes = {} 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")}.docs"] = 0
excludes["rootFolder#{Array(i).join(".folder")}.fileRefs"] = 0 excludes["rootFolder#{Array(i).join(".folder")}.fileRefs"] = 0
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) -> db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
callback error, projects[0] callback error, projects[0]
getProject: (query, projection, callback = (error, project) ->) -> getProject: (query, projection, callback = (error, project) ->) ->
if !query?
return callback("no query provided")
if typeof(projection) == "function"
callback = projection
if typeof query == "string" if typeof query == "string"
query = _id: ObjectId(query) query = _id: ObjectId(query)
else if query instanceof ObjectId else if query instanceof ObjectId
query = _id: query 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) ->) -> populateProjectWithUsers: (project, callback=(error, project) ->) ->
# eventually this should be in a UserGetter.getUser module # eventually this should be in a UserGetter.getUser module

View file

@ -1,11 +1,16 @@
Project = require('../../models/Project').Project Project = require('../../models/Project').Project
ProjectGetter = require("./ProjectGetter")
Errors = require "../../errors" Errors = require "../../errors"
_ = require('underscore') _ = require('underscore')
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
async = require('async') async = require('async')
module.exports = module.exports = ProjectLocator =
findElement: (options, callback = (err, element, path, parentFolder)->)-> findElement: (options, _callback = (err, element, path, parentFolder)->)->
callback = (args...) ->
_callback(args...)
_callback = () ->
{project, project_id, element_id, type} = options {project, project_id, element_id, type} = options
elementType = sanitizeTypeOfElement type elementType = sanitizeTypeOfElement type
@ -46,7 +51,7 @@ module.exports =
if project? if project?
startSearch(project) startSearch(project)
else else
Project.findById project_id, (err, project)-> ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
return callback(err) if err? return callback(err) if err?
if !project? if !project?
return callback(new Errors.NotFoundError("project not found")) return callback(new Errors.NotFoundError("project not found"))
@ -62,8 +67,12 @@ module.exports =
if project? if project?
getRootDoc project getRootDoc project
else else
Project.findById project_id, (err, project)-> ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
getRootDoc project if err?
logger.err err:err, "error getting project"
return callback(err)
else
getRootDoc project
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)-> findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
@ -122,11 +131,11 @@ module.exports =
async.waterfall jobs, callback async.waterfall jobs, callback
findUsersProjectByName: (user_id, projectName, 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) projects = projects.concat(collabertions)
projectName = projectName.toLowerCase() projectName = projectName.toLowerCase()
project = _.find projects, (project)-> 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" logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name"
callback(null, project) 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) -> rateLimit: (opts) ->
return (req, res, next) -> return (req, res, next) ->
if req.session.user? if req.session?.user?
user_id = req.session.user._id user_id = req.session.user._id
else else
user_id = req.ip 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) request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
.on "error", (error) -> .on "error", (error) ->
logger.error err: error, "Spelling API error" logger.error err: error, "Spelling API error"
res.sendStatus 500
.pipe(res) .pipe(res)

View file

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

View file

@ -52,11 +52,17 @@ module.exports =
return callback(err) if err? return callback(err) if err?
callback err, subscriptions.length > 0, subscriptions callback err, subscriptions.length > 0, subscriptions
hasGroupMembersLimitReached: (user_id, callback)-> hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)->
SubscriptionLocator.getUsersSubscription user_id, (err, 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 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" 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)-> getOwnerOfProject = (project_id, callback)->
Project.findById project_id, 'owner_ref', (error, project) -> Project.findById project_id, 'owner_ref', (error, project) ->

View file

@ -247,6 +247,18 @@ module.exports = RecurlyWrapper =
callback(error) 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) -> _parseSubscriptionXml: (xml, callback) ->
@_parseXml xml, (error, data) -> @_parseXml xml, (error, data) ->

View file

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

View file

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

View file

@ -1,10 +1,7 @@
SubscriptionGroupHandler = require("./SubscriptionGroupHandler") SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator") SubscriptionLocator = require("./SubscriptionLocator")
ErrorsController = require("../Errors/ErrorController") ErrorsController = require("../Errors/ErrorController")
settings = require("settings-sharelatex")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler") SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
_ = require("underscore") _ = require("underscore")
@ -12,9 +9,12 @@ module.exports =
addUserToGroup: (req, res)-> addUserToGroup: (req, res)->
adminUserId = req.session.user._id 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" logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)-> SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
if err?
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
return res.sendStatus 500
result = result =
user:user user:user
if err and err.limitReached if err and err.limitReached
@ -25,7 +25,20 @@ module.exports =
adminUserId = req.session.user._id adminUserId = req.session.user._id
userToRemove_id = req.params.user_id userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" 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() res.send()
renderSubscriptionGroupAdminPage: (req, res)-> renderSubscriptionGroupAdminPage: (req, res)->
@ -70,13 +83,16 @@ module.exports =
subscription_id = req.params.subscription_id subscription_id = req.params.subscription_id
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)? if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
return ErrorsController.notFound(req, res) 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" 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? else if err?
res.sendStatus 500 return res.sendStatus 500
else 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)-> renderSuccessfulJoinPage: (req, res)->
subscription_id = req.params.subscription_id subscription_id = req.params.subscription_id

View file

@ -9,15 +9,32 @@ logger = require("logger-sharelatex")
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler") OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
EmailHandler = require("../Email/EmailHandler") EmailHandler = require("../Email/EmailHandler")
settings = require("settings-sharelatex") settings = require("settings-sharelatex")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = SubscriptionGroupHandler = 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)-> 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 if limitReached
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
return callback(limitReached:limitReached) 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) userViewModel = buildUserViewModel(user)
callback(err, userViewModel) callback(err, userViewModel)
@ -60,13 +77,20 @@ module.exports = SubscriptionGroupHandler =
EmailHandler.sendEmail "completeJoinGroupAccount", opts, callback EmailHandler.sendEmail "completeJoinGroupAccount", opts, callback
processGroupVerification: (userEmail, subscription_id, token, 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)-> OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, token_subscription_id)->
if err? or subscription_id != 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" logger.err userEmail:userEmail, token:token, "token value not found for processing group verification"
return callback("token_not_found") return callback("token_not_found")
SubscriptionLocator.getSubscription subscription_id, (err, subscription)-> 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)-> buildUserViewModel = (user)->

View file

@ -74,5 +74,5 @@ module.exports =
return callback("no user found") return callback("no user found")
SubscriptionUpdater.syncSubscription recurlySubscription, user?._id, callback 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 Subscription.findOne _id:subscription_id, callback
getSubscriptionByMemberIdAndId: (user_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.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv 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/: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.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage
webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup 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/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription 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.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan

View file

@ -32,6 +32,9 @@ module.exports =
insertOperation = insertOperation =
"$addToSet": {member_ids:user_id} "$addToSet": {member_ids:user_id}
Subscription.findAndModify searchOps, insertOperation, (err, subscription)-> 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 UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
removeUserFromGroup: (adminUser_id, user_id, callback)-> removeUserFromGroup: (adminUser_id, user_id, callback)->
@ -39,10 +42,12 @@ module.exports =
admin_id: adminUser_id admin_id: adminUser_id
removeOperation = removeOperation =
"$pull": {member_ids:user_id} "$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 UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback
_createNewSubscription: (adminUser_id, callback)-> _createNewSubscription: (adminUser_id, callback)->
logger.log adminUser_id:adminUser_id, "creating new subscription" logger.log adminUser_id:adminUser_id, "creating new subscription"
subscription = new Subscription(admin_id:adminUser_id) subscription = new Subscription(admin_id:adminUser_id)

View file

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

View file

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

View file

@ -2,20 +2,53 @@ TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
module.exports = module.exports =
getAllTags: (req, res, next)->
processTagsUpdate: (req, res)->
user_id = req.session.user._id user_id = req.session.user._id
project_id = req.params.project_id logger.log {user_id}, "getting tags"
if req.body.deletedTag? TagsHandler.getAllTags user_id, (error, allTags)->
tag = req.body.deletedTag return next(error) if error?
TagsHandler.deleteTag user_id, project_id, tag, -> res.json(allTags)
res.send()
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 else
tag = req.body.tag logger.log {user_id, tag_id, name}, "renaming tag"
TagsHandler.addTag user_id, project_id, tag, -> TagsHandler.renameTag user_id, tag_id, name, (error) ->
res.send() return next(error) if error?
logger.log user_id:user_id, project_id:project_id, body:req.body, "processing tag update" res.status(204).end()
getAllTags: (req, res)->
TagsHandler.getAllTags req.session.user._id, (err, allTags)->
res.send(allTags)

View file

@ -3,61 +3,84 @@ settings = require("settings-sharelatex")
request = require("request") request = require("request")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
oneSecond = 1000 TIMEOUT = 1000
module.exports = module.exports = TagsHandler =
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)
getAllTags: (user_id, callback)-> getAllTags: (user_id, callback)->
@requestTags user_id, (err, allTags)=> @_requestTags user_id, (err, allTags)=>
if !allTags? if !allTags?
allTags = [] allTags = []
@groupTagsByProject allTags, (err, groupedByProject)-> @_groupTagsByProject allTags, (err, groupedByProject)->
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "getting all tags from tags api" logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "got all tags from tags api"
callback err, allTags, groupedByProject callback err, allTags, groupedByProject
createTag: (user_id, name, callback = (error, tag) ->) ->
opts =
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 {})
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)-> removeProjectFromAllTags: (user_id, project_id, callback)->
uri = buildUri(user_id, project_id) url = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
opts = opts =
uri:"#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}" url: url
timeout:oneSecond timeout:TIMEOUT
logger.log user_id:user_id, project_id:project_id, "removing project_id from tags" request.del opts, (err, res, body) ->
request.del opts, callback TagsHandler._handleResponse err, res, {url, user_id, project_id}, callback
groupTagsByProject: (tags, 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 = {} result = {}
_.each tags, (tag)-> _.each tags, (tag)->
_.each tag.project_ids, (project_id)-> _.each tag.project_ids, (project_id)->
@ -69,7 +92,3 @@ module.exports =
delete clonedTag.project_ids delete clonedTag.project_ids
result[project_id].push(clonedTag) result[project_id].push(clonedTag)
callback null, result 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 path = "/" + req.params[0] # UpdateMerger expects leading slash
source = req.headers["x-sl-update-source"] or "unknown" source = req.headers["x-sl-update-source"] or "unknown"
logger.log project_id: project_id, path: path, source: source, "received project contents update" 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? return next(error) if error?
res.sendStatus(200) res.sendStatus(200)

View file

@ -27,7 +27,7 @@ module.exports =
FileTypeManager.shouldIgnore path, (err, shouldIgnore)-> FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
if shouldIgnore if shouldIgnore
return callback() 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)-> deleteUpdate: (user_id, projectName, path, source, callback)->

View file

@ -42,6 +42,9 @@ module.exports = TpdsUpdateSender =
_addEntity: (options, callback = (err)->)-> _addEntity: (options, callback = (err)->)->
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)-> 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" 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 = postOptions =
method : "post" method : "post"
@ -53,6 +56,9 @@ module.exports = TpdsUpdateSender =
title: "addFile" title: "addFile"
streamOrigin : options.streamOrigin streamOrigin : options.streamOrigin
TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)-> 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" 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) callback(err)

View file

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

View file

@ -1,21 +1,23 @@
child = require "child_process" child = require "child_process"
logger = require "logger-sharelatex" logger = require "logger-sharelatex"
metrics = require "../../infrastructure/Metrics" metrics = require "../../infrastructure/Metrics"
fs = require "fs"
Path = require "path"
_ = require("underscore")
ONE_MEG = 1024 * 1024
module.exports = ArchiveManager = 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 unzip = child.spawn("unzip", ["-l", source])
# us to listen on this for some unknow reason
output = ""
unzip.stdout.on "data", (d)-> unzip.stdout.on "data", (d)->
output += d
error = null error = null
unzip.stderr.on "data", (chunk) -> unzip.stderr.on "data", (chunk) ->
@ -29,9 +31,80 @@ module.exports = ArchiveManager =
callback(err) callback(err)
unzip.on "exit", () -> unzip.on "exit", () ->
timer.done()
if error? if error?
error = new Error(error) error = new Error(error)
logger.error err:error, source: source, destination: destination, "error unzipping file" logger.error err:error, source: source, destination: destination, "error checking zip size"
callback(error)
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" FileTypeManager = require "./FileTypeManager"
EditorController = require "../Editor/EditorController" EditorController = require "../Editor/EditorController"
ProjectLocator = require "../Project/ProjectLocator" ProjectLocator = require "../Project/ProjectLocator"
logger = require("logger-sharelatex")
module.exports = FileSystemImportManager = module.exports = FileSystemImportManager =
addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )-> addDoc: (user_id, project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
fs.readFile path, "utf8", (error, content = "") -> FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
return callback(error) if error? if !isSafe
content = content.replace(/\r/g, "") 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"
lines = content.split("\n") return callback("path is symlink")
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback fs.readFile path, "utf8", (error, content = "") ->
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) ->
return callback(error) if error? return callback(error) if error?
return callback(new Error("Couldn't find folder")) if !folder? content = content.replace(/\r/g, "")
existingFile = null lines = content.split("\n")
for fileRef in folder.fileRefs if replace
if fileRef.name == name ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
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) =>
return callback(error) if error? return callback(error) if error?
if !ignore return callback(new Error("Couldn't find folder")) if !folder?
@addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback 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 else
callback() EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
async.parallelLimit jobs, 5, callback else
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) -> addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )->
FileTypeManager.isDirectory path, (error, isDirectory) => FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
return callback(error) if error? if !isSafe
if isDirectory 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"
@addFolder project_id, folder_id, name, path, replace, callback return callback("path is symlink")
if !replace
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
else 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? return callback(error) if error?
if isBinary return callback(new Error("Couldn't find folder")) if !folder?
@addFile project_id, folder_id, name, path, replace, callback 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 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 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" logger.err project_id:project_id, name:name, "bad name when trying to upload file"
return res.send success: false 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, -> fs.unlink path, ->
timer.done() timer.done()
if error? if error?

View file

@ -9,19 +9,21 @@ module.exports = ProjectUploadHandler =
createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) -> createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) ->
ProjectCreationHandler.createBlankProject owner_id, name, (error, project) => ProjectCreationHandler.createBlankProject owner_id, name, (error, project) =>
return callback(error) if error? 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? return callback(error) if error?
ProjectRootDocManager.setRootDocAutomatically project._id, (error) -> ProjectRootDocManager.setRootDocAutomatically project._id, (error) ->
return callback(error) if error? return callback(error) if error?
callback(error, project) callback(error, project)
insertZipArchiveIntoFolder: (project_id, folder_id, path, callback = (error) ->) -> insertZipArchiveIntoFolder: (owner_id, project_id, folder_id, path, callback = (error) ->) ->
destination = @_getDestinationDirectory path destination = @_getDestinationDirectory path
ArchiveManager.extractZipArchive path, destination, (error) -> ArchiveManager.extractZipArchive path, destination, (error) ->
return callback(error) if 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? 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) -> _getDestinationDirectory: (source) ->
return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}") return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}")

View file

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

View file

@ -1,3 +1,4 @@
UserHandler = require("./UserHandler")
UserDeleter = require("./UserDeleter") UserDeleter = require("./UserDeleter")
UserLocator = require("./UserLocator") UserLocator = require("./UserLocator")
User = require("../../models/User").User User = require("../../models/User").User
@ -8,12 +9,9 @@ metrics = require("../../infrastructure/Metrics")
Url = require("url") Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager") AuthenticationManager = require("../Authentication/AuthenticationManager")
UserUpdater = require("./UserUpdater") UserUpdater = require("./UserUpdater")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
settings = require "settings-sharelatex" settings = require "settings-sharelatex"
crypto = require "crypto"
module.exports = module.exports = UserController =
deleteUser: (req, res)-> deleteUser: (req, res)->
user_id = req.session.user._id user_id = req.session.user._id
@ -66,11 +64,18 @@ module.exports =
if err? if err?
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address" logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
if err.message == "alread_exists" if err.message == "alread_exists"
message = req.i18n.translate("alread_exists") message = req.i18n.translate("email_already_registered")
else else
message = req.i18n.translate("problem_changing_email_address") message = req.i18n.translate("problem_changing_email_address")
return res.send 500, {message:message} 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)-> logout : (req, res)->
metrics.inc "user.logout" metrics.inc "user.logout"
@ -85,32 +90,12 @@ module.exports =
if !email? or email == "" if !email? or email == ""
res.sendStatus 422 # Unprocessable Entity res.sendStatus 422 # Unprocessable Entity
return return
logger.log {email}, "registering new user" UserRegistrationHandler.registerNewUserAndSendActivationEmail email, (error, user, setNewPasswordUrl) ->
UserRegistrationHandler.registerNewUser { return next(error) if error?
email: email res.json {
password: crypto.randomBytes(32).toString("hex") email: user.email
}, (err, user)-> setNewPasswordUrl: setNewPasswordUrl
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
}
changePassword : (req, res, next = (error) ->)-> changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change" 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") UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
ErrorController = require("../Errors/ErrorController")
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
Settings = require("settings-sharelatex") Settings = require("settings-sharelatex")
fs = require('fs') fs = require('fs')
@ -20,11 +22,33 @@ module.exports =
sharedProjectData: sharedProjectData sharedProjectData: sharedProjectData
newTemplateData: newTemplateData newTemplateData: newTemplateData
new_email:req.query.new_email || "" 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)-> loginPage : (req, res)->
res.render 'user/login', res.render 'user/login',
title: 'login', title: 'login',
redir: req.query.redir redir: req.query.redir,
email: req.query.email
settingsPage : (req, res, next)-> settingsPage : (req, res, next)->
logger.log user: req.session.user, "loading settings page" logger.log user: req.session.user, "loading settings page"

View file

@ -5,8 +5,12 @@ AuthenticationManager = require("../Authentication/AuthenticationManager")
NewsLetterManager = require("../Newsletter/NewsletterManager") NewsLetterManager = require("../Newsletter/NewsletterManager")
async = require("async") async = require("async")
logger = require("logger-sharelatex") 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) -> 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,}))$/ 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) return re.test(email)
@ -58,6 +62,31 @@ module.exports =
], (err)-> ], (err)->
logger.log user: user, "registered" logger.log user: user, "registered"
callback(err, user) 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}main.js",
"#{jsPath}libs.js", "#{jsPath}libs.js",
"#{jsPath}ace/ace.js", "#{jsPath}ace/ace.js",
"#{jsPath}libs/pdfjs-1.0.1040/pdf.js", "#{jsPath}libs/pdfjs-1.3.91/pdf.js",
"#{jsPath}libs/pdfjs-1.0.1040/pdf.worker.js", "#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.0.1040/compatibility.js", "#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
"/stylesheets/style.css" "/stylesheets/style.css"
] ]
filePath = Path.join __dirname, "../../../", "public#{path}" filePath = Path.join __dirname, "../../../", "public#{path}"

View file

@ -31,6 +31,7 @@ ProjectSchema = new Schema
description : {type:String, default:''} description : {type:String, default:''}
archived : { type: Boolean } archived : { type: Boolean }
deletedDocs : [DeletedDocSchema] deletedDocs : [DeletedDocSchema]
imageName : { type: String }
ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
if project_or_id._id? if project_or_id._id?
@ -65,51 +66,7 @@ ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)
this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=> this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=>
callback(err, projects, collabertions, 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 } github: { type:Boolean, default: Settings.defaultFeatures.github }
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout } compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup } compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
references: { type:Boolean, default: Settings.defaultFeatures.references }
} }
featureSwitches : { featureSwitches : {
pdfng: { type: Boolean } pdfng: { type: Boolean }

View file

@ -16,6 +16,7 @@ ReferalController = require('./Features/Referal/ReferalController')
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware') ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
AuthenticationController = require('./Features/Authentication/AuthenticationController') AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController") TagsController = require("./Features/Tags/TagsController")
NotificationsController = require("./Features/Notifications/NotificationsController")
CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter')
UserInfoController = require('./Features/User/UserInfoController') UserInfoController = require('./Features/User/UserInfoController')
UserController = require("./Features/User/UserController") UserController = require("./Features/User/UserController")
@ -37,6 +38,7 @@ RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController") InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
ContactRouter = require("./Features/Contacts/ContactRouter") ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
_ = require("underscore") _ = require("underscore")
@ -46,7 +48,7 @@ module.exports = class Router
if !Settings.allowPublicAccess if !Settings.allowPublicAccess
webRouter.all '*', AuthenticationController.requireGlobalLogin webRouter.all '*', AuthenticationController.requireGlobalLogin
webRouter.get '/login', UserPagesController.loginPage webRouter.get '/login', UserPagesController.loginPage
AuthenticationController.addEndpointToLoginWhitelist '/login' AuthenticationController.addEndpointToLoginWhitelist '/login'
@ -67,16 +69,18 @@ module.exports = class Router
StaticPagesRouter.apply(webRouter, apiRouter) StaticPagesRouter.apply(webRouter, apiRouter)
RealTimeProxyRouter.apply(webRouter, apiRouter) RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter) ContactRouter.apply(webRouter, apiRouter)
Modules.applyRouter(webRouter, apiRouter) Modules.applyRouter(webRouter, apiRouter)
if Settings.enableSubscriptions if Settings.enableSubscriptions
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
webRouter.get '/blog', BlogController.getIndexPage webRouter.get '/blog', BlogController.getIndexPage
webRouter.get '/blog/*', BlogController.getPage webRouter.get '/blog/*', BlogController.getPage
webRouter.get '/user/activate', UserPagesController.activateAccountPage
webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword 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/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
webRouter.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate 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 # Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
@ -156,7 +167,7 @@ module.exports = class Router
apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate
apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate
apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents
apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents apiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents
@ -165,9 +176,12 @@ module.exports = class Router
webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages webRouter.get "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.getMessages
webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage webRouter.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
webRouter.get /learn(\/.*)?/, WikiController.getPage 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 #Admin Stuff
webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index 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 webRouter.get '/admin/user', SecurityManager.requestIsAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon
@ -186,7 +200,7 @@ module.exports = class Router
apiRouter.get '/status', (req,res)-> apiRouter.get '/status', (req,res)->
res.send("websharelatex is up") res.send("websharelatex is up")
webRouter.get '/health_check', HealthCheckController.check webRouter.get '/health_check', HealthCheckController.check
webRouter.get '/health_check/redis', HealthCheckController.checkRedis webRouter.get '/health_check/redis', HealthCheckController.checkRedis
@ -222,4 +236,4 @@ module.exports = class Router
logger.error err: req.body.error, meta: req.body.meta, "client side error" logger.error err: req.body.error, meta: req.body.meta, "client side error"
res.sendStatus(204) res.sendStatus(204)
webRouter.get '*', ErrorController.notFound webRouter.get '*', ErrorController.notFound

View file

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

View file

@ -6,6 +6,7 @@ div.full-size(
resize-on="layout:main:resize" resize-on="layout:main:resize"
resize-proportionally="true" resize-proportionally="true"
initial-size-east="'50%'" initial-size-east="'50%'"
minimum-restore-size-east="300"
) )
.ui-layout-center .ui-layout-center
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening") .loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
@ -19,6 +20,7 @@ div.full-size(
keybindings="settings.mode", keybindings="settings.mode",
font-size="settings.fontSize", font-size="settings.fontSize",
auto-complete="settings.autoComplete", auto-complete="settings.autoComplete",
spell-check="true",
spell-check-language="project.spellCheckLanguage", spell-check-language="project.spellCheckLanguage",
highlights="onlineUserCursorHighlights[editor.open_doc_id]" highlights="onlineUserCursorHighlights[editor.open_doc_id]"
show-print-margin="false", 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") .toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write")
a( a(
href, href,
@ -27,7 +27,8 @@ aside#file-tree(ng-controller="FileTreeController").full-size
href, href,
ng-click="startRenamingSelected()", ng-click="startRenamingSelected()",
tooltip="#{translate('rename')}", tooltip="#{translate('rename')}",
tooltip-placement="bottom" tooltip-placement="bottom",
ng-show="multiSelectedCount == 0"
) )
i.fa.fa-pencil i.fa.fa-pencil
a( a(
@ -45,26 +46,24 @@ aside#file-tree(ng-controller="FileTreeController").full-size
ng-controller="FileTreeRootFolderController", ng-controller="FileTreeRootFolderController",
ng-class="{ 'no-toolbar': !permissions.write }" 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( ul.list-unstyled.file-tree-list(
droppable="permissions.write" droppable="permissions.write"
accept=".entity-name" accept=".entity-name"
on-drop-callback="onDrop" 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( file-entity(
entity="entity", entity="entity",
permissions="permissions", permissions="permissions",
@ -81,27 +80,25 @@ aside#file-tree(ng-controller="FileTreeController").full-size
) )
.entity .entity
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
) )
//- Just a spacer to align with folders //- Just a spacer to align with folders
i.fa.fa-fw.toggle i.fa.fa-fw.toggle
i.fa.fa-fw.fa-file i.fa.fa-fw.fa-file
span {{ entity.name }} span {{ entity.name }}
script(type='text/ng-template', id='entityListItemTemplate') script(type='text/ng-template', id='entityListItemTemplate')
li( li(
ng-class="{ 'selected': entity.selected }", ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }",
ng-controller="FileTreeEntityController" ng-controller="FileTreeEntityController"
) )
.entity(ng-if="entity.type != 'folder'") .entity(ng-if="entity.type != 'folder'")
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()" ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write" draggable="permissions.write"
draggable-helper="draggableHelper"
context-menu context-menu
data-target="context-menu-{{ entity.id }}" data-target="context-menu-{{ entity.id }}"
context-menu-container="body" 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-file(ng-if="entity.type == 'doc'")
i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'") i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'")
span( span(
ng-hide="entity.renaming" ng-hide="entity.renaming"
) {{ entity.name }} ) {{ entity.name }}
@ -126,12 +122,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()" on-enter="finishRenaming()"
) )
span.dropdown( span.dropdown.entity-menu-toggle(
dropdown, dropdown,
ng-show="entity.selected",
ng-if="permissions.write" ng-if="permissions.write"
) )
a.dropdown-toggle(href, dropdown-toggle) a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
i.fa.fa-chevron-down i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right ul.dropdown-menu.dropdown-menu-right
@ -140,12 +135,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
div.dropdown.context-menu( div.dropdown.context-menu(
@ -158,20 +155,23 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
.entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController") .entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController")
.entity-name( .entity-name(
ng-click="select()" ng-click="select($event)"
ng-dblclick="permissions.write && startRenaming()" ng-dblclick="permissions.write && startRenaming()"
draggable="permissions.write" draggable="permissions.write"
draggable-helper="draggableHelper"
droppable="permissions.write" droppable="permissions.write"
accept=".entity-name" accept=".entity-name"
on-drop-callback="onDrop" on-drop-callback="onDrop"
@ -194,7 +194,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
'fa-folder': !expanded, \ 'fa-folder': !expanded, \
'fa-folder-open': expanded \ 'fa-folder-open': expanded \
}" }"
ng-click="select()" ng-click="select($event)"
) )
span( span(
@ -210,12 +210,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
on-enter="finishRenaming()" on-enter="finishRenaming()"
) )
span.dropdown( span.dropdown.entity-menu-toggle(
dropdown, dropdown,
ng-if="permissions.write" 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 i.fa.fa-chevron-down
ul.dropdown-menu.dropdown-menu-right ul.dropdown-menu.dropdown-menu-right
@ -224,12 +223,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
li.divider li.divider
li li
@ -261,12 +262,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
href href
ng-click="startRenaming()" ng-click="startRenaming()"
right-click="startRenaming()" right-click="startRenaming()"
ng-show="!entity.multiSelected"
) #{translate("rename")} ) #{translate("rename")}
li li
a( a(
href href
ng-click="openDeleteModal()" ng-click="openDeleteModal()"
right-click="openDeleteModal()" right-click="openDeleteModal()"
stop-propagation="click"
) #{translate("delete")} ) #{translate("delete")}
li.divider li.divider
li li
@ -306,6 +309,7 @@ script(type='text/ng-template', id='newDocModalTemplate')
h3 #{translate("new_file")} h3 #{translate("new_file")}
.modal-body .modal-body
form(novalidate, name="newDocForm") form(novalidate, name="newDocForm")
div.alert.alert-danger(ng-if="error") {{error}}
input.form-control( input.form-control(
type="text", type="text",
placeholder="File Name", placeholder="File Name",
@ -330,6 +334,7 @@ script(type='text/ng-template', id='newFolderModalTemplate')
.modal-header .modal-header
h3 #{translate("new_folder")} h3 #{translate("new_folder")}
.modal-body .modal-body
div.alert.alert-danger(ng-if="error") {{error}}
form(novalidate, name="newFolderForm") form(novalidate, name="newFolderForm")
input.form-control( input.form-control(
type="text", type="text",
@ -354,9 +359,20 @@ script(type='text/ng-template', id='newFolderModalTemplate')
script(type="text/ng-template", id="uploadFileModalTemplate") script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-header .modal-header
h3 #{translate("upload_files")} h3 #{translate("upload_files")}
span &nbsp; .alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
.alert.alert-warning.small(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(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( .modal-body(
fine-upload fine-upload
@ -367,10 +383,14 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
drag-area-text="{{drag_files}}" drag-area-text="{{drag_files}}"
hint-text="{{hint_press_and_hold_control_key}}" hint-text="{{hint_press_and_hold_control_key}}"
multiple="true" multiple="true"
auto-upload="false"
on-complete-callback="onComplete" on-complete-callback="onComplete"
on-upload-callback="onUpload" on-upload-callback="onUpload"
on-validate-batch="onValidateBatch" on-validate-batch="onValidateBatch"
on-error-callback="onError" on-error-callback="onError"
on-submit-callback="onSubmit"
on-cancel-callback="onCancel"
control="control"
params="{'folder_id': parent_folder_id}" params="{'folder_id': parent_folder_id}"
) )
span #{translate("upload_files")} span #{translate("upload_files")}
@ -382,6 +402,8 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
h3 #{translate("delete")} {{ entity.name }} h3 #{translate("delete")} {{ entity.name }}
.modal-body .modal-body
p !{translate("sure_you_want_to_delete")} p !{translate("sure_you_want_to_delete")}
ul
li(ng-repeat="entity in entities") {{entity.name}}
.modal-footer .modal-footer
button.btn.btn-default( button.btn.btn-default(
ng-disabled="state.inflight" ng-disabled="state.inflight"

View file

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

View file

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

View file

@ -6,46 +6,84 @@ script(type="text/ng-template", id="publishProjectAsTemplateModalTemplate")
ng-click="cancel()" ng-click="cancel()"
) &times; ) &times;
h3 #{translate("publish_as_template")} h3 #{translate("publish_as_template")}
.modal-body.modal-body-share div(ng-if="project.features.templates")
span(ng-hide="problemTalkingToTemplateApi") .modal-body.modal-body-share
form() span(ng-hide="problemTalkingToTemplateApi")
label(for='Description') #{translate("template_description")} form()
.form-group label(for='Description') #{translate("template_description")}
textarea.form-control( .form-group
rows=5, textarea.form-control(
name='Description', rows=5,
ng-model="templateDetails.description", name='Description',
value="" ng-model="templateDetails.description",
) value=""
div(ng-show="templateDetails.exists").text-center.templateDetails )
| #{translate("project_last_published_at")} div(ng-show="templateDetails.exists").text-center.templateDetails
strong {{templateDetails.publishedDate}}. | #{translate("project_last_published_at")}
a(ng-href="{{templateDetails.canonicalUrl}}") #{translate("view_in_template_gallery")}. 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")}...
div(ng-hide="project.features.templates")
.modal-body.modal-body-share
p #{translate("upgrade_to_get_feature", {feature:"templates"})}
.modal-footer(ng-hide="problemTalkingToTemplateApi") ul.list-unstyled
button.btn.btn-default( li
ng-click="cancel()", i.fa.fa-check &nbsp;
ng-disabled="state.publishInflight || state.unpublishInflight" | #{translate("unlimited_projects")}
)
span #{translate("cancel")} 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")}
button.btn.btn-info( li
ng-click="unpublishTemplate()", i.fa.fa-check &nbsp;
ng-disabled="state.publishInflight || state.unpublishInflight" | #{translate("sync_to_github")}
ng-show="templateDetails.exists"
)
span(ng-show="!state.unpublishInflight") #{translate("unpublish")}
span(ng-show="state.unpublishInflight") #{translate("unpublishing")}...
button.btn.btn-primary( li
ng-click="publishTemplate()", i.fa.fa-check &nbsp;
ng-disabled="state.publishInflight || state.unpublishInflight" |#{translate("compile_larger_projects")}
)
span(ng-show="!state.publishInflight && !templateDetails.exists") #{translate("publish")} .modal-footer(ng-controller="FreeTrialModalController")
span(ng-show="!state.publishInflight && templateDetails.exists") #{translate("republish")} .text-center
span(ng-show="state.publishInflight") #{translate("publishing")}... 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" type="submit"
ng-mousedown="addMembers()" ng-mousedown="addMembers()"
) #{translate("share")} ) #{translate("share")}
div.text-center(ng-hide="canAddCollaborators") div(ng-hide="canAddCollaborators")
p #{translate("need_to_upgrade_for_more_collabs")}. p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
h4 .row
i.fa.fa-check &nbsp; .col-md-8.col-md-offset-2
| #{translate("unlimited_projects")} ul.list-unstyled
li
h4 i.fa.fa-check &nbsp;
i.fa.fa-check &nbsp; | #{translate("unlimited_projects")}
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
li
h4 i.fa.fa-check &nbsp;
i.fa.fa-check &nbsp; | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
| #{translate("full_doc_history")}
li
h4 i.fa.fa-check &nbsp;
i.fa.fa-check &nbsp; | #{translate("full_doc_history")}
| #{translate("sync_to_dropbox")}
li
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("sync_to_github")} | #{translate("sync_to_github")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")} |#{translate("compile_larger_projects")}
p(ng-controller="FreeTrialModalController") p.text-center.row-spaced-thin(ng-controller="FreeTrialModalController")
a.btn.btn-success( a.btn.btn-success(
href href
ng-class="buttonClass" 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")} span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-primary( button.btn.btn-primary(
ng-click="done()" ng-click="done()"
) #{translate("done")} ) #{translate("close")}
script(type="text/ng-template", id="makePublicModalTemplate") script(type="text/ng-template", id="makePublicModalTemplate")
.modal-header .modal-header

View file

@ -1,83 +1,39 @@
div#trackChanges(ng-show="ui.view == 'track-changes'") div#trackChanges(ng-show="ui.view == 'track-changes'")
span(ng-controller="TrackChangesPremiumPopup") span(ng-controller="TrackChangesPremiumPopup")
span(ng-if="versioningPopupType == 'default'") .upgrade-prompt(ng-show="!project.features.versioning")
.upgrade-prompt(ng-show="!project.features.versioning") .message(ng-show="project.owner._id == user.id")
.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")}
p #{translate("upgrade_to_get_feature", {feature:"Entire Doc History"})} ul.list-unstyled
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")} li
h4
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")} | #{translate("unlimited_projects")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("collabs_per_proj", {collabcount:'Multiple'})} | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("full_doc_history")} | #{translate("full_doc_history")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")} | #{translate("sync_to_dropbox")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
| #{translate("sync_to_github")} | #{translate("sync_to_github")}
h4 li
i.fa.fa-check &nbsp; i.fa.fa-check &nbsp;
|#{translate("compile_larger_projects")} |#{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')"
) #{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")
a.btn.btn-success( a.btn.btn-success(
href href
ng-class="buttonClass" ng-class="buttonClass"
ng-click="startFreeTrial('track-changes', 'cf3yutfzu7ztxz')" ng-click="startFreeTrial('track-changes')"
) #{translate("start_free_trial")} ) #{translate("start_free_trial")}

View file

@ -9,7 +9,8 @@ block content
script(type="text/javascript"). script(type="text/javascript").
window.data = { window.data = {
projects: !{JSON.stringify(projects).replace(/\//g, '\\/')}, 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 = { window.algolia = {
institutions: { institutions: {
@ -20,12 +21,14 @@ block content
.content.content-alt(ng-controller="ProjectPageController") .content.content-alt(ng-controller="ProjectPageController")
.container .container
.row(ng-cloak) .row(ng-cloak)
span(ng-show="first_sign_up == 'default' || projects.length > 0") span(ng-show="first_sign_up == 'default' || projects.length > 0")
aside.col-md-2.col-xs-3 aside.col-md-2.col-xs-3
include ./list/side-bar include ./list/side-bar
.col-md-10.col-xs-9 .col-md-10.col-xs-9
include ./list/notifications
include ./list/project-list include ./list/project-list
span(ng-if="first_sign_up == 'minimial' && projects.length == 0") 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" stop-propagation="click"
) )
.modal-footer .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 //- We stop propagation to stop the clicks from closing the
//- 'move to folder' menu. //- 'move to folder' menu.
button.btn.btn-default( button.btn.btn-default(
@ -25,10 +27,67 @@ script(type='text/ng-template', id='newTagModalTemplate')
stop-propagation="click" stop-propagation="click"
) #{translate("cancel")} ) #{translate("cancel")}
button.btn.btn-primary( button.btn.btn-primary(
ng-disabled="newTagForm.$invalid" ng-disabled="newTagForm.$invalid || state.inflight"
ng-click="create()" ng-click="create()"
stop-propagation="click" 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') script(type='text/ng-template', id='renameProjectModalTemplate')
.modal-header .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.dropdown-header #{translate("add_to_folder")}
li( li(
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'", ng-repeat="tag in tags | orderBy:'name'",
ng-controller="TagDropdownItemController" ng-controller="TagDropdownItemController"
) )
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
@ -79,7 +79,7 @@
href='#', href='#',
data-toggle="dropdown", data-toggle="dropdown",
dropdown-toggle dropdown-toggle
) #{translate("more")} ) #{translate("more")}
span.caret span.caret
ul.dropdown-menu.dropdown-menu-right(role="menu") ul.dropdown-menu.dropdown-menu-right(role="menu")
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")

View file

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

View file

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

View file

@ -8,7 +8,7 @@ block content
.card .card
.page-header .page-header
h1 #{translate("your_subscription")} 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 &nbsp;
div div
-if(subscription.groupPlan) -if(subscription.groupPlan)

View file

@ -6,7 +6,7 @@ block scripts
window.recomendedCurrency = '#{recomendedCurrency}' window.recomendedCurrency = '#{recomendedCurrency}'
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}" window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
window.taxRate = #{taxRate} window.taxRate = #{taxRate}
window.subscription = !{JSON.stringify(subscription)}
@ -36,32 +36,32 @@ mixin printPlans(plans)
mixin printPlan(plan) mixin printPlan(plan)
block content block content
.content.content-alt .content.content-alt(ng-cloak)
.container .container(ng-controller="UserSubscriptionController")
.row .row
.col-md-8.col-md-offset-2 .col-md-8.col-md-offset-2
.card .card(ng-if="view == 'overview'")
.page-header .page-header
h1 #{translate("your_subscription")} h1 #{translate("your_subscription")}
-if (groups.length != 0) -if (groups.length != 0)
each groupSubscription in groups each groupSubscription in groups
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})} p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
span
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
- else if (subscription) - else if (subscription)
case subscription.state case subscription.state
when "free-trial" when "free-trial"
p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})} p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})}
p !{translate("choose_a_plan_below")}
when "active" when "active"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})} p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
a(href, ng-click="changePlan = true") !{translate("change_plan")}. 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 !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
p.pull-right p.pull-right
p
p: form(action="/user/subscription/cancel",method="post")
input(type="hidden", name="_csrf", value=csrfToken)
a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")} a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")}
| &nbsp; | &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" when "canceled"
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})} p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"<strong>" + subscription.nextPaymentDueAt + "</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")} p !{translate("problem_with_subscription_contact_us")}
-if(subscription.groupPlan) -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.individualMonthlyPlans)
mixin printPlans(plans.individualAnnualPlans) 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"). script(type="text/javascript").
$('#cancelSubscription').on("click", function() { $('#cancelSubscription').on("click", function() {
ga('send', 'event', 'subscription-funnel', 'cancelation') ga('send', 'event', 'subscription-funnel', 'cancelation')
}) })
script(type='text/ng-template', id='confirmChangePlanModalTemplate') script(type='text/ng-template', id='confirmChangePlanModalTemplate')
.modal-header .modal-header
h3 Change plan? h3 #{translate("change_plan")}
.modal-body .modal-body
p !{translate("sure_you_want_to_change_plan", {planName:"<strong>{{plan.name}}</strong>"})} p !{translate("sure_you_want_to_change_plan", {planName:"<strong>{{plan.name}}</strong>"})}
.modal-footer .modal-footer
@ -131,4 +164,22 @@ block content
span(ng-show="inflight") #{translate("processing")}... 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' : ''") .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') select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
option(value="", disabled, selected) Year option(value="", disabled, selected) Year
option(value="2015") 2015
option(value="2016") 2016 option(value="2016") 2016
option(value="2017") 2017 option(value="2017") 2017
option(value="2018") 2018 option(value="2018") 2018

View file

@ -3,6 +3,12 @@ block scripts
script(type='text/javascript'). script(type='text/javascript').
window.recomendedCurrency = '#{recomendedCurrency}' window.recomendedCurrency = '#{recomendedCurrency}'
window.abCurrencyFlag = '#{abCurrencyFlag}' 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 block content
.content-alt .content-alt
.content.plans(ng-controller="PlansController") .content.plans(ng-controller="PlansController")
@ -186,30 +192,30 @@ block content
.modal-header .modal-header
h3 #{translate("group_plan_enquiry")} h3 #{translate("group_plan_enquiry")}
.modal-body .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.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
.form-group span(ng-show="sent == false")
label(for='Field9') #{translate("name")} .form-group
input.form-control(name='Field9', type='text', value='', maxlength='255', tabindex='1', onkeyup='') label#title9(for='Field9')
| Name
.form-group input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
label(for='Field11') #{translate("email")} label#title11.desc(for='Field11')
input.form-control(name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2') | Email
.form-group
.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(for='Field12') #{translate("university")} label#title12.desc(for='Field12')
input.form-control(name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='') | University / Company
.form-group
.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(for='Field13') #{translate("position")} label#title13.desc(for='Field13')
input.form-control(name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='') | Position
.form-group
.form-group input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
input.btn.btn-primary.btn-large(name='saveForm', type='submit', value='Send') .form-group
div(style='display: none;') input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';")
label(for='comment') Do Not Fill This Out .form-group.text-center
textarea#comment(name='comment', rows='1', cols='1') input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
input#idstamp(type='hidden', name='idstamp', value='xkgLkZnS/AQW71jCS1d0XrrFjq26lJryIPVk2rx0YkU=') span(ng-show="sent")
p Request Sent, Thank you.
.row .row
.col-md-12 .col-md-12

View file

@ -5,14 +5,18 @@ block content
.container .container
.row .row
.col-md-8.col-md-offset-2 .col-md-8.col-md-offset-2
.card .card(ng-cloak)
.page-header .page-header
h2 #{translate("thanks_for_subscribing")} h2 #{translate("thanks_for_subscribing")}
.alert.alert-success .alert.alert-success
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>"+subscription.price+"</strong>", collectionDate:"<strong>"+subscription.nextPaymentDueAt+"</strong>"})} 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")} span(sixpack-switch="upgrade-success-message")
a(href="/user/subscription") #{translate("click_here_to_cancel")}. 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 p
- if (subscription.groupPlan == true) - if (subscription.groupPlan == true)
a.btn.btn-success.btn-large(href="/subscription/group") #{translate("add_your_first_group_member_now")} 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', placeholder='email@example.com',
ng-model="email", ng-model="email",
ng-model-options="{ updateOn: 'blur' }", ng-model-options="{ updateOn: 'blur' }",
ng-init="email = #{JSON.stringify(email)}",
focus="true" focus="true"
) )
span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty") span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ define [
"ide/permissions/PermissionsManager" "ide/permissions/PermissionsManager"
"ide/pdf/PdfManager" "ide/pdf/PdfManager"
"ide/binary-files/BinaryFilesManager" "ide/binary-files/BinaryFilesManager"
"ide/references/ReferencesManager"
"ide/settings/index" "ide/settings/index"
"ide/share/index" "ide/share/index"
"ide/chat/index" "ide/chat/index"
@ -24,6 +25,7 @@ define [
"directives/onEnter" "directives/onEnter"
"directives/stopPropagation" "directives/stopPropagation"
"directives/rightClick" "directives/rightClick"
"services/queued-http"
"filters/formatDate" "filters/formatDate"
"main/event" "main/event"
"main/account-upgrade" "main/account-upgrade"
@ -37,6 +39,7 @@ define [
PermissionsManager PermissionsManager
PdfManager PdfManager
BinaryFilesManager BinaryFilesManager
ReferencesManager
) -> ) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage) -> App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
@ -66,12 +69,13 @@ define [
$scope.chat = {} $scope.chat = {}
window._ide = ide window._ide = ide
ide.project_id = $scope.project_id = window.project_id ide.project_id = $scope.project_id = window.project_id
ide.$scope = $scope ide.$scope = $scope
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
ide.connectionManager = new ConnectionManager(ide, $scope) ide.connectionManager = new ConnectionManager(ide, $scope)
ide.fileTreeManager = new FileTreeManager(ide, $scope) ide.fileTreeManager = new FileTreeManager(ide, $scope)
ide.editorManager = new EditorManager(ide, $scope) ide.editorManager = new EditorManager(ide, $scope)
@ -80,7 +84,7 @@ define [
ide.pdfManager = new PdfManager(ide, $scope) ide.pdfManager = new PdfManager(ide, $scope)
ide.permissionsManager = new PermissionsManager(ide, $scope) ide.permissionsManager = new PermissionsManager(ide, $scope)
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope) ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
inited = false inited = false
$scope.$on "project:joined", () -> $scope.$on "project:joined", () ->
return if inited return if inited
@ -91,7 +95,7 @@ define [
We don't want to delete your data on ShareLaTeX, so this project still contains your history and collaborators. We don't want to delete your data on ShareLaTeX, so this project still contains your history and collaborators.
If the project has been renamed please look in your project list for a new project under the new name. If the project has been renamed please look in your project list for a new project under the new name.
""") """)
DARK_THEMES = [ DARK_THEMES = [
"ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers", "ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers",
"merbivore", "merbivore_soft", "mono_industrial", "monokai", "merbivore", "merbivore_soft", "mono_industrial", "monokai",

View file

@ -37,8 +37,12 @@ define [
# Restore previously recorded state # Restore previously recorded state
if (state = ide.localStorage("layout.#{name}"))? if (state = ide.localStorage("layout.#{name}"))?
options.west = state.west if state.east?
options.east = 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 = () -> repositionControls = () ->
state = element.layout().readState() state = element.layout().readState()

View file

@ -36,6 +36,7 @@ define [
@doc?.detachFromAce() @doc?.detachFromAce()
editorDoc = @ace?.getSession().getDocument() editorDoc = @ace?.getSession().getDocument()
editorDoc?.off "change", @_checkConsistency editorDoc?.off "change", @_checkConsistency
@ide.$scope.$emit 'document:closed', @doc
_checkConsistency: () -> _checkConsistency: () ->
# We've been seeing a lot of errors when I think there shouldn't be # We've been seeing a lot of errors when I think there shouldn't be
@ -116,6 +117,26 @@ define [
flush: () -> flush: () ->
@doc?.flushPendingOps() @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: () -> pollSavedStatus: () ->
# returns false if doc has ops waiting to be acknowledged or # returns false if doc has ops waiting to be acknowledged or
# sent that haven't changed since the last time we checked. # sent that haven't changed since the last time we checked.

View file

@ -86,6 +86,7 @@ define [
@_bindToDocumentEvents(doc, new_sharejs_doc) @_bindToDocumentEvents(doc, new_sharejs_doc)
callback null, new_sharejs_doc callback null, new_sharejs_doc
_bindToDocumentEvents: (doc, sharejs_doc) -> _bindToDocumentEvents: (doc, sharejs_doc) ->
sharejs_doc.on "error", (error, meta) => sharejs_doc.on "error", (error, meta) =>
if error?.message?.match "maxDocLength" if error?.message?.match "maxDocLength"
@ -98,7 +99,7 @@ define [
@ide.reportError(error, meta) @ide.reportError(error, meta)
@ide.showGenericMessageModal( @ide.showGenericMessageModal(
"Out of sync" "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) @openDoc(doc, forceReopen: true)

View file

@ -18,7 +18,7 @@ define [
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
return url return url
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage) -> App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory) ->
monkeyPatchSearch($rootScope, $compile) monkeyPatchSearch($rootScope, $compile)
return { return {
@ -29,6 +29,7 @@ define [
fontSize: "=" fontSize: "="
autoComplete: "=" autoComplete: "="
sharejsDoc: "=" sharejsDoc: "="
spellCheck: "="
spellCheckLanguage: "=" spellCheckLanguage: "="
highlights: "=" highlights: "="
text: "=" text: "="
@ -55,7 +56,9 @@ define [
scope.name = attrs.aceEditor scope.name = attrs.aceEditor
autoCompleteManager = new AutoCompleteManager(scope, editor, element) 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) undoManager = new UndoManager(scope, editor, element)
highlightsManager = new HighlightsManager(scope, editor, element) highlightsManager = new HighlightsManager(scope, editor, element)
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
@ -69,6 +72,21 @@ define [
editor.commands.removeCommand "transposeletters" editor.commands.removeCommand "transposeletters"
editor.commands.removeCommand "showSettingsMenu" editor.commands.removeCommand "showSettingsMenu"
editor.commands.removeCommand "foldall" 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 # Trigger search AND replace on CMD+F
editor.commands.addCommand editor.commands.addCommand
@ -77,7 +95,6 @@ define [
exec: (editor) -> exec: (editor) ->
ace.require("ace/ext/searchbox").Search(editor, true) ace.require("ace/ext/searchbox").Search(editor, true)
readOnly: true readOnly: true
editor.commands.removeCommand "replace"
# Bold text on CMD+B # Bold text on CMD+B
editor.commands.addCommand editor.commands.addCommand

View file

@ -16,7 +16,7 @@ define [
constructor: (@$scope, @editor) -> constructor: (@$scope, @editor) ->
@suggestionManager = new SuggestionManager() @suggestionManager = new SuggestionManager()
@monkeyPatchAutocomplete() @monkeyPatchAutocomplete()
@$scope.$watch "autoComplete", (autocomplete) => @$scope.$watch "autoComplete", (autocomplete) =>
if autocomplete if autocomplete
@ -37,11 +37,47 @@ define [
enableSnippets: true, enableSnippets: true,
enableLiveAutocompletion: false enableLiveAutocompletion: false
}) })
SnippetCompleter = SnippetCompleter =
getCompletions: (editor, session, pos, prefix, callback) -> getCompletions: (editor, session, pos, prefix, callback) ->
callback null, Snippets 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: () -> disable: () ->
@editor.setOptions({ @editor.setOptions({
@ -83,7 +119,7 @@ define [
# since it will be adding in with the autocomplete of \begin{item}... # since it will be adding in with the autocomplete of \begin{item}...
if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}" if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
editor.session.remove(range) editor.session.remove(range)
Autocomplete::_insertMatch.call this, data Autocomplete::_insertMatch.call this, data
# Overwrite this to set autoInsert = false and set font size # Overwrite this to set autoInsert = false and set font size

View file

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

View file

@ -5,7 +5,7 @@ define [
Range = ace.require("ace/range").Range Range = ace.require("ace/range").Range
class SpellCheckManager class SpellCheckManager
constructor: (@$scope, @editor, @element) -> constructor: (@$scope, @editor, @element, @cache) ->
$(document.body).append @element.find(".spell-check-menu") $(document.body).append @element.find(".spell-check-menu")
@updatedLines = [] @updatedLines = []
@ -102,6 +102,8 @@ define [
learnWord: (highlight) -> learnWord: (highlight) ->
@apiRequest "/learn", word: highlight.word @apiRequest "/learn", word: highlight.word
@highlightedWordManager.removeWord highlight.word @highlightedWordManager.removeWord highlight.word
language = @$scope.spellCheckLanguage
@cache?.put("#{language}:#{highlight.word}", true)
getHighlightedWordAtCursor: () -> getHighlightedWordAtCursor: () ->
cursor = @editor.getCursorPosition() cursor = @editor.getCursorPosition()
@ -143,24 +145,67 @@ define [
runSpellCheck: (linesToProcess) -> runSpellCheck: (linesToProcess) ->
{words, positions} = @getWords(linesToProcess) {words, positions} = @getWords(linesToProcess)
language = @$scope.spellCheckLanguage 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? if linesToProcess?
for shouldProcess, row in linesToProcess for shouldProcess, row in linesToProcess
@highlightedWordManager.clearRows(row, row) if shouldProcess @highlightedWordManager.clearRows(row, row) if shouldProcess
else else
@highlightedWordManager.clearRows() @highlightedWordManager.clearRows()
for highlight in highlights
@highlightedWordManager.addHighlight highlight
for misspelling in result.misspellings if not words.length
word = words[misspelling.index] displayResult highlights
position = positions[misspelling.index] else
@highlightedWordManager.addHighlight @apiRequest "/check", {language: language, words: words}, (error, result) =>
column: position.column if error? or !result? or !result.misspellings?
row: position.row return null
word: word mispelled = []
suggestions: misspelling.suggestions 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) -> getWords: (linesToProcess) ->
lines = @editor.getValue().split("\n") lines = @editor.getValue().split("\n")

View file

@ -19,6 +19,12 @@ define [
@recalculateDocList() @recalculateDocList()
@_bindToSocketEvents() @_bindToSocketEvents()
@$scope.multiSelectedCount = 0
$(document).on "click", =>
@clearMultiSelectedEntities()
$scope.$digest()
_bindToSocketEvents: () -> _bindToSocketEvents: () ->
@ide.socket.on "reciveNewDoc", (parent_folder_id, doc) => @ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
@ -65,6 +71,7 @@ define [
@$scope.$apply () => @$scope.$apply () =>
@_deleteEntityFromScope entity @_deleteEntityFromScope entity
@recalculateDocList() @recalculateDocList()
@$scope.$emit "entity:deleted", entity
@ide.socket.on "reciveEntityMove", (entity_id, folder_id) => @ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
entity = @findEntityById(entity_id) entity = @findEntityById(entity_id)
@ -78,7 +85,62 @@ define [
@ide.fileTreeManager.forEachEntity (entity) -> @ide.fileTreeManager.forEachEntity (entity) ->
entity.selected = false entity.selected = false
entity.selected = true 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: () -> findSelectedEntity: () ->
selected = null selected = null
@forEachEntity (entity) -> @forEachEntity (entity) ->
@ -277,7 +339,7 @@ define [
deleteEntity: (entity, callback = (error) ->) -> deleteEntity: (entity, callback = (error) ->) ->
# We'll wait for the socket.io notification to # We'll wait for the socket.io notification to
# delete from scope. # delete from scope.
return @ide.$http { return @ide.queuedHttp {
method: "DELETE" method: "DELETE"
url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}" url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}"
headers: headers:
@ -289,7 +351,7 @@ define [
# since that would break the tree structure. # since that would break the tree structure.
return if @_isChildFolder(entity, parent_folder) return if @_isChildFolder(entity, parent_folder)
@_moveEntityInScope(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 folder_id: parent_folder.id
_csrf: window.csrfToken _csrf: window.csrfToken
} }
@ -316,8 +378,6 @@ define [
entity.deleted = true entity.deleted = true
@$scope.deletedDocs.push entity @$scope.deletedDocs.push entity
@$scope.$emit "entity:deleted", entity
_moveEntityInScope: (entity, parent_folder) -> _moveEntityInScope: (entity, parent_folder) ->
return if entity in parent_folder.children return if entity in parent_folder.children
@_deleteEntityFromScope(entity, moveToDeleted: false) @_deleteEntityFromScope(entity, moveToDeleted: false)

View file

@ -61,6 +61,8 @@ define [
$scope.state.inflight = true $scope.state.inflight = true
ide.fileTreeManager ide.fileTreeManager
.createDoc(name, parent_folder) .createDoc(name, parent_folder)
.error (e)->
$scope.error = e
.success () -> .success () ->
$scope.state.inflight = false $scope.state.inflight = false
$modalInstance.close() $modalInstance.close()
@ -90,6 +92,8 @@ define [
$scope.state.inflight = true $scope.state.inflight = true
ide.fileTreeManager ide.fileTreeManager
.createFolder(name, parent_folder) .createFolder(name, parent_folder)
.error (e)->
$scope.error = e
.success () -> .success () ->
$scope.state.inflight = false $scope.state.inflight = false
$modalInstance.close() $modalInstance.close()
@ -99,17 +103,30 @@ define [
] ]
App.controller "UploadFileModalController", [ App.controller "UploadFileModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window"
($scope, ide, $modalInstance, $timeout, parent_folder) -> ($scope, ide, $modalInstance, $timeout, parent_folder, $window) ->
$scope.parent_folder_id = parent_folder?.id $scope.parent_folder_id = parent_folder?.id
$scope.tooManyFiles = false $scope.tooManyFiles = false
$scope.rateLimitHit = false $scope.rateLimitHit = false
$scope.secondsToRedirect = 10
$scope.notLoggedIn = false
$scope.conflicts = []
$scope.control = {}
uploadCount = 0 needToLogBackIn = ->
$scope.onUpload = () -> $scope.notLoggedIn = true
uploadCount++ 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) -> $scope.onComplete = (error, name, response) ->
$timeout (() -> $timeout (() ->
uploadCount-- uploadCount--
@ -127,8 +144,40 @@ define [
return true return true
$scope.onError = (id, name, reason)-> $scope.onError = (id, name, reason)->
console.log(id, name, reason)
if reason.indexOf("429") != -1 if reason.indexOf("429") != -1
$scope.rateLimitHit = true $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 = () -> $scope.cancel = () ->
$modalInstance.dismiss('cancel') $modalInstance.dismiss('cancel')

View file

@ -2,9 +2,23 @@ define [
"base" "base"
], (App) -> ], (App) ->
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) -> App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
$scope.select = () -> $scope.select = (e) ->
ide.fileTreeManager.selectEntity($scope.entity) if e.ctrlKey or e.metaKey
$scope.$emit "entity:selected", $scope.entity 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 = $scope.inputs =
name: $scope.entity.name name: $scope.entity.name
@ -24,10 +38,15 @@ define [
$scope.startRenaming() if $scope.entity.selected $scope.startRenaming() if $scope.entity.selected
$scope.openDeleteModal = () -> $scope.openDeleteModal = () ->
if ide.fileTreeManager.multiSelectedCount() > 0
entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
else
entities = [$scope.entity]
$modal.open( $modal.open(
templateUrl: "deleteEntityModalTemplate" templateUrl: "deleteEntityModalTemplate"
controller: "DeleteEntityModalController" controller: "DeleteEntityModalController"
scope: $scope resolve:
entities: () -> entities
) )
$scope.$on "delete:selected", () -> $scope.$on "delete:selected", () ->
@ -35,18 +54,18 @@ define [
] ]
App.controller "DeleteEntityModalController", [ App.controller "DeleteEntityModalController", [
"$scope", "ide", "$modalInstance", "$scope", "ide", "$modalInstance", "entities"
($scope, ide, $modalInstance) -> ($scope, ide, $modalInstance, entities) ->
$scope.state = $scope.state =
inflight: false inflight: false
$scope.entities = entities
$scope.delete = () -> $scope.delete = () ->
$scope.state.inflight = true $scope.state.inflight = true
ide.fileTreeManager for entity in $scope.entities
.deleteEntity($scope.entity) ide.fileTreeManager.deleteEntity(entity)
.success () -> $modalInstance.close()
$scope.state.inflight = false
$modalInstance.close()
$scope.cancel = () -> $scope.cancel = () ->
$modalInstance.dismiss('cancel') $modalInstance.dismiss('cancel')

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