mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' into node-4.2
This commit is contained in:
commit
f4cbcc22ba
182 changed files with 56804 additions and 3311 deletions
|
@ -95,10 +95,10 @@ module.exports = (grunt) ->
|
|||
paths:
|
||||
"moment": "libs/moment-2.9.0"
|
||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
|
||||
"libs/pdf": "libs/pdfjs-1.0.1040/pdf"
|
||||
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
|
||||
shim:
|
||||
"libs/pdf":
|
||||
deps: ["libs/pdfjs-1.0.1040/compatibility"]
|
||||
deps: ["libs/pdfjs-1.3.91/compatibility"]
|
||||
|
||||
skipDirOptimize: true
|
||||
modules: [
|
||||
|
|
|
@ -3,8 +3,12 @@ logger = require 'logger-sharelatex'
|
|||
logger.initialize("web-sharelatex")
|
||||
logger.logger.serializers.user = require("./app/js/infrastructure/LoggerSerializers").user
|
||||
logger.logger.serializers.project = require("./app/js/infrastructure/LoggerSerializers").project
|
||||
if Settings.sentry?.dsn?
|
||||
logger.initializeErrorReporting(Settings.sentry.dsn)
|
||||
|
||||
metrics = require("metrics-sharelatex")
|
||||
metrics.initialize("web")
|
||||
metrics.memory.monitor(logger)
|
||||
Server = require("./app/js/infrastructure/Server")
|
||||
Errors = require "./app/js/errors"
|
||||
|
||||
|
@ -15,6 +19,10 @@ argv = require("optimist")
|
|||
.argv
|
||||
|
||||
Server.app.use (error, req, res, next) ->
|
||||
if error?.code is 'EBADCSRFTOKEN'
|
||||
logger.log err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf"
|
||||
res.sendStatus(403)
|
||||
return
|
||||
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
|
||||
res.statusCode = error.status or 500
|
||||
if res.statusCode == 500
|
||||
|
|
|
@ -8,13 +8,16 @@ querystring = require('querystring')
|
|||
Url = require("url")
|
||||
Settings = require "settings-sharelatex"
|
||||
basicAuth = require('basic-auth-connect')
|
||||
|
||||
UserHandler = require("../User/UserHandler")
|
||||
|
||||
module.exports = AuthenticationController =
|
||||
login: (req, res, next = (error) ->) ->
|
||||
email = req.body?.email?.toLowerCase()
|
||||
password = req.body?.password
|
||||
redir = Url.parse(req.body?.redir or "/project").path
|
||||
AuthenticationController.doLogin req.body, req, res, next
|
||||
|
||||
doLogin: (options, req, res, next) ->
|
||||
email = options.email?.toLowerCase()
|
||||
password = options.password
|
||||
redir = Url.parse(options.redir or "/project").path
|
||||
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
|
||||
if !isAllowed
|
||||
logger.log email:email, "too many login requests"
|
||||
|
@ -26,17 +29,18 @@ module.exports = AuthenticationController =
|
|||
AuthenticationManager.authenticate email: email, password, (error, user) ->
|
||||
return next(error) if error?
|
||||
if user?
|
||||
UserHandler.setupLoginData user, ->
|
||||
LoginRateLimiter.recordSuccessfulLogin email
|
||||
AuthenticationController._recordSuccessfulLogin user._id
|
||||
AuthenticationController.establishUserSession req, user, (error) ->
|
||||
return next(error) if error?
|
||||
req.session.justLoggedIn = true
|
||||
logger.log email: email, user_id: user._id.toString(), "successful log in"
|
||||
res.send redir: redir
|
||||
res.json redir: redir
|
||||
else
|
||||
AuthenticationController._recordFailedLogin()
|
||||
logger.log email: email, "failed log in"
|
||||
res.send message:
|
||||
res.json message:
|
||||
text: req.i18n.translate("email_or_password_wrong_try_again"),
|
||||
type: 'error'
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
User = require("../../models/User").User
|
||||
{db, ObjectId} = require("../../infrastructure/mongojs")
|
||||
crypto = require 'crypto'
|
||||
|
|
|
@ -4,15 +4,13 @@ logger = require("logger-sharelatex")
|
|||
_ = require("underscore")
|
||||
ErrorController = require "../Errors/ErrorController"
|
||||
|
||||
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps"]
|
||||
|
||||
module.exports = BlogController =
|
||||
|
||||
getPage: (req, res, next)->
|
||||
url = req.url?.toLowerCase()
|
||||
blogUrl = "#{settings.apis.blog.url}#{url}"
|
||||
|
||||
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps"]
|
||||
extensionsToProxy = [".png", ".xml", ".jpeg", ".json", ".zip", ".eps", ".gif"]
|
||||
|
||||
shouldProxy = _.find extensionsToProxy, (extension)->
|
||||
url.indexOf(extension) != -1
|
||||
|
|
|
@ -58,8 +58,8 @@ module.exports = ClsiManager =
|
|||
return outputFiles
|
||||
|
||||
VALID_COMPILERS: ["pdflatex", "latex", "xelatex", "lualatex"]
|
||||
_buildRequest: (project_id, settingsOverride={}, callback = (error, request) ->) ->
|
||||
Project.findById project_id, {compiler: 1, rootDoc_id: 1}, (error, project) ->
|
||||
_buildRequest: (project_id, options={}, callback = (error, request) ->) ->
|
||||
Project.findById project_id, {compiler: 1, rootDoc_id: 1, imageName: 1}, (error, project) ->
|
||||
return callback(error) if error?
|
||||
return callback(new Errors.NotFoundError("project does not exist: #{project_id}")) if !project?
|
||||
|
||||
|
@ -82,7 +82,7 @@ module.exports = ClsiManager =
|
|||
content: doc.lines.join("\n")
|
||||
if project.rootDoc_id? and doc._id.toString() == project.rootDoc_id.toString()
|
||||
rootResourcePath = path
|
||||
if settingsOverride.rootDoc_id? and doc._id.toString() == settingsOverride.rootDoc_id.toString()
|
||||
if options.rootDoc_id? and doc._id.toString() == options.rootDoc_id.toString()
|
||||
rootResourcePathOverride = path
|
||||
|
||||
rootResourcePath = rootResourcePathOverride if rootResourcePathOverride?
|
||||
|
@ -101,7 +101,9 @@ module.exports = ClsiManager =
|
|||
compile:
|
||||
options:
|
||||
compiler: project.compiler
|
||||
timeout: settingsOverride.timeout
|
||||
timeout: options.timeout
|
||||
imageName: project.imageName
|
||||
draft: !!options.draft
|
||||
rootResourcePath: rootResourcePath
|
||||
resources: resources
|
||||
}
|
||||
|
@ -110,8 +112,11 @@ module.exports = ClsiManager =
|
|||
ClsiManager._buildRequest project_id, options, (error, req) ->
|
||||
compilerUrl = ClsiManager._getCompilerUrl(options?.compileGroup)
|
||||
filename = file || req?.compile?.rootResourcePath
|
||||
wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}"
|
||||
if req.compile.options.imageName?
|
||||
wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}"
|
||||
request.get {
|
||||
url: "#{compilerUrl}/project/#{project_id}/wordcount?file=#{filename}"
|
||||
url: wordcount_url
|
||||
}, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
if 200 <= response.statusCode < 300
|
||||
|
|
|
@ -25,6 +25,8 @@ module.exports = CompileController =
|
|||
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
|
||||
if req.body?.compiler
|
||||
options.compiler = req.body.compiler
|
||||
if req.body?.draft
|
||||
options.draft = req.body.draft
|
||||
logger.log {options, project_id}, "got compile request"
|
||||
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, output, limits) ->
|
||||
return next(error) if error?
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
request = require 'request'
|
||||
request = request.defaults()
|
||||
async = require 'async'
|
||||
settings = require 'settings-sharelatex'
|
||||
_ = require 'underscore'
|
||||
async = require 'async'
|
||||
|
@ -116,7 +115,7 @@ module.exports = DocumentUpdaterHandler =
|
|||
logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
|
||||
callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
|
||||
|
||||
setDocument : (project_id, doc_id, docLines, source, callback = (error) ->)->
|
||||
setDocument : (project_id, doc_id, user_id, docLines, source, callback = (error) ->)->
|
||||
timer = new metrics.Timer("set-document")
|
||||
url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}"
|
||||
body =
|
||||
|
@ -124,7 +123,8 @@ module.exports = DocumentUpdaterHandler =
|
|||
json:
|
||||
lines: docLines
|
||||
source: source
|
||||
logger.log project_id:project_id, doc_id: doc_id, source: source, "setting doc in document updater"
|
||||
user_id: user_id
|
||||
logger.log project_id:project_id, doc_id: doc_id, source: source, user_id: user_id, "setting doc in document updater"
|
||||
request.post body, (error, res, body)->
|
||||
timer.done()
|
||||
if error?
|
||||
|
|
|
@ -6,15 +6,20 @@ module.exports =
|
|||
getDocument: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
doc_id = req.params.doc_id
|
||||
plain = req?.query?.plain == 'true'
|
||||
logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)"
|
||||
ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev) ->
|
||||
if error?
|
||||
logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument"
|
||||
return next(error)
|
||||
res.type "json"
|
||||
res.send JSON.stringify {
|
||||
lines: lines
|
||||
}
|
||||
if plain
|
||||
res.type "text/plain"
|
||||
res.send lines.join('\n')
|
||||
else
|
||||
res.type "json"
|
||||
res.send JSON.stringify {
|
||||
lines: lines
|
||||
}
|
||||
|
||||
setDocument: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
|
|
|
@ -13,8 +13,8 @@ LockManager = require("../../infrastructure/LockManager")
|
|||
_ = require('underscore')
|
||||
|
||||
module.exports = EditorController =
|
||||
setDoc: (project_id, doc_id, docLines, source, callback = (err)->)->
|
||||
DocumentUpdaterHandler.setDocument project_id, doc_id, docLines, source, (err)=>
|
||||
setDoc: (project_id, doc_id, user_id, docLines, source, callback = (err)->)->
|
||||
DocumentUpdaterHandler.setDocument project_id, doc_id, user_id, docLines, source, (err)=>
|
||||
logger.log project_id:project_id, doc_id:doc_id, "notifying users that the document has been updated"
|
||||
DocumentUpdaterHandler.flushDocToMongo project_id, doc_id, callback
|
||||
|
||||
|
@ -33,10 +33,12 @@ module.exports = EditorController =
|
|||
logger.log {project_id, folder_id, docName, source}, "sending new doc to project"
|
||||
Metrics.inc "editor.add-doc"
|
||||
ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, (err, doc, folder_id)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, docName:docName, "error adding doc without lock"
|
||||
return callback(err)
|
||||
EditorRealTimeController.emitToRoom(project_id, 'reciveNewDoc', folder_id, doc, source)
|
||||
callback(err, doc)
|
||||
|
||||
|
||||
addFile: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
|
||||
LockManager.getLock project_id, (err)->
|
||||
if err?
|
||||
|
@ -46,20 +48,20 @@ module.exports = EditorController =
|
|||
LockManager.releaseLock project_id, ->
|
||||
callback(error, file)
|
||||
|
||||
|
||||
addFileWithoutLock: (project_id, folder_id, fileName, path, source, callback = (error, file)->)->
|
||||
fileName = fileName.trim()
|
||||
logger.log {project_id, folder_id, fileName, path}, "sending new file to project"
|
||||
Metrics.inc "editor.add-file"
|
||||
ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, folder_id:folder_id, fileName:fileName, "error adding file without lock"
|
||||
return callback(err)
|
||||
EditorRealTimeController.emitToRoom(project_id, 'reciveNewFile', folder_id, fileRef, source)
|
||||
callback(err, fileRef)
|
||||
|
||||
replaceFile: (project_id, file_id, fsPath, source, callback = (error) ->)->
|
||||
ProjectEntityHandler.replaceFile project_id, file_id, fsPath, callback
|
||||
|
||||
|
||||
|
||||
addFolder : (project_id, folder_id, folderName, source, callback = (error, folder)->)->
|
||||
LockManager.getLock project_id, (err)->
|
||||
if err?
|
||||
|
@ -74,6 +76,9 @@ module.exports = EditorController =
|
|||
logger.log {project_id, folder_id, folderName, source}, "sending new folder to project"
|
||||
Metrics.inc "editor.add-folder"
|
||||
ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, folder_id:folder_id, folderName:folderName, "error adding folder without lock"
|
||||
return callback(err)
|
||||
@p.notifyProjectUsersOfNewFolder project_id, folder_id, folder, (error) ->
|
||||
callback error, folder
|
||||
|
||||
|
@ -90,6 +95,9 @@ module.exports = EditorController =
|
|||
mkdirpWithoutLock: (project_id, path, callback)->
|
||||
logger.log project_id:project_id, path:path, "making directories if they don't exist"
|
||||
ProjectEntityHandler.mkdirp project_id, path, (err, newFolders, lastFolder)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, path:path, "error mkdirp without lock"
|
||||
return callback(err)
|
||||
self = @
|
||||
jobs = _.map newFolders, (folder, index)->
|
||||
return (cb)->
|
||||
|
@ -109,7 +117,10 @@ module.exports = EditorController =
|
|||
deleteEntityWithoutLock: (project_id, entity_id, entityType, source, callback)->
|
||||
logger.log {project_id, entity_id, entityType, source}, "start delete process of entity"
|
||||
Metrics.inc "editor.delete-entity"
|
||||
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, =>
|
||||
ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity"
|
||||
return callback(err)
|
||||
logger.log project_id:project_id, entity_id:entity_id, entityType:entityType, "telling users entity has been deleted"
|
||||
EditorRealTimeController.emitToRoom(project_id, 'removeEntity', entity_id, source)
|
||||
if callback?
|
||||
|
@ -143,19 +154,28 @@ module.exports = EditorController =
|
|||
newName = sanitize.escape(newName)
|
||||
Metrics.inc "editor.rename-entity"
|
||||
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
|
||||
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, =>
|
||||
ProjectEntityHandler.renameEntity project_id, entity_id, entityType, newName, ->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, newName:newName, "error renaming entity"
|
||||
return callback(err)
|
||||
if newName.length > 0
|
||||
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
|
||||
callback?()
|
||||
#
|
||||
|
||||
moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
|
||||
Metrics.inc "editor.move-entity"
|
||||
ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, entity_id:entity_id, folder_id:folder_id, "error moving entity"
|
||||
return callback(err)
|
||||
EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
|
||||
callback?()
|
||||
|
||||
renameProject: (project_id, newName, callback = (err) ->) ->
|
||||
ProjectDetailsHandler.renameProject project_id, newName, =>
|
||||
ProjectDetailsHandler.renameProject project_id, newName, ->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, newName:newName, "error renaming project"
|
||||
return callback(err)
|
||||
EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', newName
|
||||
callback()
|
||||
|
||||
|
|
|
@ -67,11 +67,16 @@ module.exports = EditorHttpController =
|
|||
project_id = req.params.Project_id
|
||||
name = req.body.name
|
||||
parent_folder_id = req.body.parent_folder_id
|
||||
logger.log project_id:project_id, name:name, parent_folder_id:parent_folder_id, "getting request to add doc to project"
|
||||
if !EditorHttpController._nameIsAcceptableLength(name)
|
||||
return res.sendStatus 400
|
||||
EditorController.addDoc project_id, parent_folder_id, name, [], "editor", (error, doc) ->
|
||||
return next(error) if error?
|
||||
res.json doc
|
||||
if error == "project_has_to_many_files"
|
||||
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
|
||||
else if error?
|
||||
next(error)
|
||||
else
|
||||
res.json doc
|
||||
|
||||
addFolder: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
|
@ -80,8 +85,12 @@ module.exports = EditorHttpController =
|
|||
if !EditorHttpController._nameIsAcceptableLength(name)
|
||||
return res.sendStatus 400
|
||||
EditorController.addFolder project_id, parent_folder_id, name, "editor", (error, doc) ->
|
||||
return next(error) if error?
|
||||
res.json doc
|
||||
if error == "project_has_to_many_files"
|
||||
res.status(400).json(req.i18n.translate("project_has_to_many_files"))
|
||||
else if error?
|
||||
next(error)
|
||||
else
|
||||
res.json doc
|
||||
|
||||
renameEntity: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
_ = require('underscore')
|
||||
|
||||
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
||||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
settings = require("settings-sharelatex")
|
||||
|
@ -15,8 +14,6 @@ templates.registered =
|
|||
|
||||
<p><a href="<%= setNewPasswordUrl %>">Click here to set your password and log in.</a></p>
|
||||
|
||||
<p>Once you have reset your password you can <a href="#{settings.siteUrl}/login">log in here</a>.</p>
|
||||
|
||||
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
logger = require('logger-sharelatex')
|
||||
metrics = require('../../infrastructure/Metrics')
|
||||
Settings = require('settings-sharelatex')
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
nodemailer = require("nodemailer")
|
||||
sesTransport = require('nodemailer-ses-transport')
|
||||
_ = require("underscore")
|
||||
|
||||
if Settings.email? and Settings.email.fromAddress?
|
||||
defaultFromAddress = Settings.email.fromAddress
|
||||
|
@ -15,15 +16,24 @@ client =
|
|||
logger.log options:options, "Would send email if enabled."
|
||||
callback()
|
||||
|
||||
if Settings.email?
|
||||
if Settings.email.transport? and Settings.email.parameters?
|
||||
nm_client = nodemailer.createTransport( Settings.email.transport, Settings.email.parameters )
|
||||
if nm_client
|
||||
client = nm_client
|
||||
else
|
||||
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
||||
else
|
||||
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
|
||||
if Settings?.email?.parameters?.AWSAccessKeyID?
|
||||
logger.log "using aws ses for email"
|
||||
nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters))
|
||||
else if Settings?.email?.parameters?
|
||||
smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth")
|
||||
|
||||
|
||||
logger.log "using smtp for email"
|
||||
nm_client = nodemailer.createTransport(smtp)
|
||||
else
|
||||
nm_client = client
|
||||
logger.warn "Email transport and/or parameters not defined. No emails will be sent."
|
||||
|
||||
if nm_client?
|
||||
client = nm_client
|
||||
else
|
||||
logger.warn "Failed to create email transport. Please check your settings. No email will be sent."
|
||||
|
||||
|
||||
module.exports =
|
||||
sendEmail : (options, callback = (error) ->)->
|
||||
|
@ -42,4 +52,3 @@ module.exports =
|
|||
else
|
||||
logger.log "Message sent to #{options.to}"
|
||||
callback(err)
|
||||
|
||||
|
|
|
@ -6,24 +6,32 @@ settings = require("settings-sharelatex")
|
|||
oneMinInMs = 60 * 1000
|
||||
fiveMinsInMs = oneMinInMs * 5
|
||||
|
||||
module.exports =
|
||||
module.exports = FileStoreHandler =
|
||||
|
||||
uploadFileFromDisk: (project_id, file_id, fsPath, callback)->
|
||||
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
|
||||
readStream = fs.createReadStream(fsPath)
|
||||
opts =
|
||||
method: "post"
|
||||
uri: @_buildUrl(project_id, file_id)
|
||||
timeout:fiveMinsInMs
|
||||
writeStream = request(opts)
|
||||
readStream.pipe writeStream
|
||||
writeStream.on "end", callback
|
||||
readStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
|
||||
callback err
|
||||
writeStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
|
||||
callback err
|
||||
fs.lstat fsPath, (err, stat)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "error stating file"
|
||||
callback(err)
|
||||
if !stat.isFile()
|
||||
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "tried to upload symlink, not contining"
|
||||
return callback(new Error("can not upload symlink"))
|
||||
|
||||
logger.log project_id:project_id, file_id:file_id, fsPath:fsPath, "uploading file from disk"
|
||||
readStream = fs.createReadStream(fsPath)
|
||||
opts =
|
||||
method: "post"
|
||||
uri: FileStoreHandler._buildUrl(project_id, file_id)
|
||||
timeout:fiveMinsInMs
|
||||
writeStream = request(opts)
|
||||
readStream.pipe writeStream
|
||||
writeStream.on "end", callback
|
||||
readStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the read stream of uploadFileFromDisk"
|
||||
callback err
|
||||
writeStream.on "error", (err)->
|
||||
logger.err err:err, project_id:project_id, file_id:file_id, fsPath:fsPath, "something went wrong on the write stream of uploadFileFromDisk"
|
||||
callback err
|
||||
|
||||
getFileStream: (project_id, file_id, query, callback)->
|
||||
logger.log project_id:project_id, file_id:file_id, query:query, "getting file stream from file store"
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -1,5 +1,7 @@
|
|||
PasswordResetHandler = require("./PasswordResetHandler")
|
||||
RateLimiter = require("../../infrastructure/RateLimiter")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
UserGetter = require("../User/UserGetter")
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports =
|
||||
|
@ -37,14 +39,19 @@ module.exports =
|
|||
title:"set_password"
|
||||
passwordResetToken: req.session.resetToken
|
||||
|
||||
setNewUserPassword: (req, res)->
|
||||
setNewUserPassword: (req, res, next)->
|
||||
{passwordResetToken, password} = req.body
|
||||
if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0
|
||||
return res.sendStatus 400
|
||||
delete req.session.resetToken
|
||||
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) ->
|
||||
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
|
||||
return next(err) if err?
|
||||
if found
|
||||
res.sendStatus 200
|
||||
if req.body.login_after
|
||||
UserGetter.getUser user_id, {email: 1}, (err, user) ->
|
||||
return next(err) if err?
|
||||
AuthenticationController.doLogin {email:user.email, password: password}, req, res, next
|
||||
else
|
||||
res.sendStatus 200
|
||||
else
|
||||
res.send 404, {message: req.i18n.translate("password_reset_token_expired")}
|
||||
res.sendStatus 404
|
||||
|
|
|
@ -23,11 +23,11 @@ module.exports =
|
|||
return callback(error) if error?
|
||||
callback null, true
|
||||
|
||||
setNewUserPassword: (token, password, callback = (error, found) ->)->
|
||||
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
|
||||
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)->
|
||||
if err then return callback(err)
|
||||
if !user_id?
|
||||
return callback null, false
|
||||
return callback null, false, null
|
||||
AuthenticationManager.setUserPassword user_id, password, (err) ->
|
||||
if err then return callback(err)
|
||||
callback null, true
|
||||
callback null, true, user_id
|
|
@ -9,6 +9,7 @@ Project = require('../../models/Project').Project
|
|||
User = require('../../models/User').User
|
||||
TagsHandler = require("../Tags/TagsHandler")
|
||||
SubscriptionLocator = require("../Subscription/SubscriptionLocator")
|
||||
NotificationsHandler = require("../Notifications/NotificationsHandler")
|
||||
LimitationsManager = require("../Subscription/LimitationsManager")
|
||||
_ = require("underscore")
|
||||
Settings = require("settings-sharelatex")
|
||||
|
@ -51,7 +52,7 @@ module.exports = ProjectController =
|
|||
deleteProject: (req, res) ->
|
||||
project_id = req.params.Project_id
|
||||
forever = req.query?.forever?
|
||||
logger.log project_id: project_id, forever: forever, "received request to delete project"
|
||||
logger.log project_id: project_id, forever: forever, "received request to archive project"
|
||||
|
||||
if forever
|
||||
doDelete = projectDeleter.deleteProject
|
||||
|
@ -125,6 +126,8 @@ module.exports = ProjectController =
|
|||
async.parallel {
|
||||
tags: (cb)->
|
||||
TagsHandler.getAllTags user_id, cb
|
||||
notifications: (cb)->
|
||||
NotificationsHandler.getUserNotifications user_id, cb
|
||||
projects: (cb)->
|
||||
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
|
||||
hasSubscription: (cb)->
|
||||
|
@ -137,6 +140,9 @@ module.exports = ProjectController =
|
|||
return next(err)
|
||||
logger.log results:results, user_id:user_id, "rendering project list"
|
||||
tags = results.tags[0]
|
||||
notifications = require("underscore").map results.notifications, (notification)->
|
||||
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
|
||||
return notification
|
||||
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
|
||||
user = results.user
|
||||
ProjectController._injectProjectOwners projects, (error, projects) ->
|
||||
|
@ -147,6 +153,7 @@ module.exports = ProjectController =
|
|||
priority_title: true
|
||||
projects: projects
|
||||
tags: tags
|
||||
notifications: notifications or []
|
||||
user: user
|
||||
hasSubscription: results.hasSubscription[0]
|
||||
}
|
||||
|
@ -168,15 +175,15 @@ module.exports = ProjectController =
|
|||
return res.render("general/closed", {title:"updating_site"})
|
||||
|
||||
if req.session.user?
|
||||
user_id = req.session.user._id
|
||||
user_id = req.session.user._id
|
||||
anonymous = false
|
||||
else
|
||||
anonymous = true
|
||||
user_id = 'openUser'
|
||||
|
||||
|
||||
project_id = req.params.Project_id
|
||||
logger.log project_id:project_id, "loading editor"
|
||||
|
||||
|
||||
async.parallel {
|
||||
project: (cb)->
|
||||
Project.findPopulatedById project_id, cb
|
||||
|
@ -193,7 +200,7 @@ module.exports = ProjectController =
|
|||
SubscriptionLocator.getUsersSubscription user_id, cb
|
||||
activate: (cb)->
|
||||
InactiveProjectManager.reactivateProjectIfRequired project_id, cb
|
||||
markAsOpened: (cb)->
|
||||
markAsOpened: (cb)->
|
||||
#don't need to wait for this to complete
|
||||
ProjectUpdateHandler.markAsOpened project_id, ->
|
||||
cb()
|
||||
|
@ -205,6 +212,7 @@ module.exports = ProjectController =
|
|||
user = results.user
|
||||
subscription = results.subscription
|
||||
|
||||
|
||||
daysSinceLastUpdated = (new Date() - project.lastUpdated) /86400000
|
||||
logger.log project_id:project_id, daysSinceLastUpdated:daysSinceLastUpdated, "got db results for loading editor"
|
||||
|
||||
|
@ -214,6 +222,7 @@ module.exports = ProjectController =
|
|||
|
||||
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
|
||||
allowedFreeTrial = !!subscription.freeTrial.allowed || true
|
||||
|
||||
logger.log project_id:project_id, "rendering editor page"
|
||||
res.render 'project/editor',
|
||||
title: project.name
|
||||
|
@ -310,5 +319,4 @@ do generateThemeList = () ->
|
|||
for file in files
|
||||
if file.slice(-2) == "js" and file.match(/^theme-/)
|
||||
cleanName = file.slice(0,-3).slice(6)
|
||||
THEME_LIST.push cleanName
|
||||
|
||||
THEME_LIST.push cleanName
|
|
@ -19,6 +19,8 @@ module.exports =
|
|||
project = new Project
|
||||
owner_ref : new ObjectId(owner_id)
|
||||
name : projectName
|
||||
if Settings.currentImageName?
|
||||
project.imageName = Settings.currentImageName
|
||||
project.rootFolder[0] = rootFolder
|
||||
User.findById owner_id, "ace.spellCheckLanguage", (err, user)->
|
||||
project.spellCheckLanguage = user.ace.spellCheckLanguage
|
||||
|
@ -33,7 +35,9 @@ module.exports =
|
|||
self._buildTemplate "mainbasic.tex", owner_id, projectName, (error, docLines)->
|
||||
return callback(error) if error?
|
||||
ProjectEntityHandler.addDoc project._id, project.rootFolder[0]._id, "main.tex", docLines, (error, doc)->
|
||||
return callback(error) if error?
|
||||
if error?
|
||||
logger.err err:error, "error adding doc when creating basic project"
|
||||
return callback(error)
|
||||
ProjectEntityHandler.setRootDoc project._id, doc._id, (error) ->
|
||||
callback(error, project)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
Project = require('../../models/Project').Project
|
||||
ProjectGetter = require("./ProjectGetter")
|
||||
logger = require('logger-sharelatex')
|
||||
documentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
|
||||
tagsHandler = require("../Tags/TagsHandler")
|
||||
|
@ -33,10 +34,10 @@ module.exports = ProjectDeleter =
|
|||
Project.remove _id: project_id, callback
|
||||
|
||||
archiveProject: (project_id, callback = (error) ->)->
|
||||
logger.log project_id:project_id, "deleting project"
|
||||
Project.findById project_id, (err, project)=>
|
||||
logger.log project_id:project_id, "archived project from user request"
|
||||
ProjectGetter.getProject project_id, {owner_ref:true, collaberator_refs:true, readOnly_refs:true}, (err, project)=>
|
||||
if err? or !project?
|
||||
logger.err err:err, project_id:project_id, "error getting project to delete it"
|
||||
logger.err err:err, project_id:project_id, "error getting project to archived it"
|
||||
callback(err)
|
||||
else
|
||||
async.series [
|
||||
|
@ -57,8 +58,10 @@ module.exports = ProjectDeleter =
|
|||
Project.update {_id:project_id}, { $set: { archived: true }}, cb
|
||||
], (err)->
|
||||
if err?
|
||||
logger.err err:err, "problem deleting project"
|
||||
callback(err)
|
||||
logger.err err:err, "problem archived project"
|
||||
return callback(err)
|
||||
logger.log project_id:project_id, "succesfully archived project from user request"
|
||||
callback()
|
||||
|
||||
restoreProject: (project_id, callback = (error) ->) ->
|
||||
Project.update {_id:project_id}, { $unset: { archived: true }}, callback
|
||||
|
|
|
@ -8,7 +8,7 @@ _ = require("underscore")
|
|||
module.exports =
|
||||
|
||||
getDetails: (project_id, callback)->
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||
ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true}, (err, project)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, "error getting project"
|
||||
return callback(err)
|
||||
|
@ -37,7 +37,7 @@ module.exports =
|
|||
|
||||
renameProject: (project_id, newName, callback = ->)->
|
||||
logger.log project_id: project_id, newName:newName, "renaming project"
|
||||
ProjectGetter.getProject project_id, {"name":1}, (err, project)->
|
||||
ProjectGetter.getProject project_id, {name:true}, (err, project)->
|
||||
if err? or !project?
|
||||
logger.err err:err, project_id:project_id, "error getting project or could not find it todo project rename"
|
||||
return callback(err)
|
||||
|
|
|
@ -4,61 +4,86 @@ projectLocator = require('./ProjectLocator')
|
|||
projectOptionsHandler = require('./ProjectOptionsHandler')
|
||||
DocumentUpdaterHandler = require("../DocumentUpdater/DocumentUpdaterHandler")
|
||||
DocstoreManager = require "../Docstore/DocstoreManager"
|
||||
Project = require("../../models/Project").Project
|
||||
ProjectGetter = require("./ProjectGetter")
|
||||
_ = require('underscore')
|
||||
async = require('async')
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
duplicate: (owner, originalProjectId, newProjectName, callback)->
|
||||
DocumentUpdaterHandler.flushProjectToMongo originalProjectId, (err) ->
|
||||
return callback(err) if err?
|
||||
Project.findById originalProjectId, (err, originalProject) ->
|
||||
return callback(err) if err?
|
||||
projectCreationHandler.createBlankProject owner._id, newProjectName, (err, newProject)->
|
||||
return callback(err) if err?
|
||||
projectLocator.findRootDoc {project:originalProject}, (err, originalRootDoc)->
|
||||
return callback(err) if err?
|
||||
DocstoreManager.getAllDocs originalProjectId, (err, docContentsArray) ->
|
||||
return callback(err) if err?
|
||||
|
||||
docContents = {}
|
||||
for docContent in docContentsArray
|
||||
docContents[docContent._id] = docContent
|
||||
module.exports = ProjectDuplicator =
|
||||
|
||||
projectOptionsHandler.setCompiler newProject._id, originalProject.compiler
|
||||
_copyDocs: (newProject, originalRootDoc, originalFolder, desFolder, docContents, callback)->
|
||||
setRootDoc = _.once (doc_id)->
|
||||
projectEntityHandler.setRootDoc newProject._id, doc_id
|
||||
|
||||
setRootDoc = _.once (doc_id)->
|
||||
projectEntityHandler.setRootDoc newProject, doc_id
|
||||
jobs = originalFolder.docs.map (doc)->
|
||||
return (cb)->
|
||||
content = docContents[doc._id.toString()]
|
||||
projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, (err, newDoc)->
|
||||
if err?
|
||||
logger.err err:err, "error copying doc"
|
||||
return callback(err)
|
||||
if originalRootDoc? and newDoc.name == originalRootDoc.name
|
||||
setRootDoc newDoc._id
|
||||
cb()
|
||||
|
||||
copyDocs = (originalFolder, newParentFolder, callback)->
|
||||
jobs = originalFolder.docs.map (doc)->
|
||||
return (callback)->
|
||||
content = docContents[doc._id.toString()]
|
||||
return callback(new Error("doc_id not found: #{doc._id}")) if !content?
|
||||
projectEntityHandler.addDoc newProject, newParentFolder._id, doc.name, content.lines, (err, newDoc)->
|
||||
if originalRootDoc? and newDoc.name == originalRootDoc.name
|
||||
setRootDoc newDoc._id
|
||||
callback()
|
||||
async.series jobs, callback
|
||||
async.series jobs, callback
|
||||
|
||||
copyFiles = (originalFolder, newParentFolder, callback)->
|
||||
jobs = originalFolder.fileRefs.map (file)->
|
||||
return (callback)->
|
||||
projectEntityHandler.copyFileFromExistingProject newProject, newParentFolder._id, originalProject._id, file, callback
|
||||
async.parallelLimit jobs, 5, callback
|
||||
_copyFiles: (newProject, originalProject_id, originalFolder, desFolder, callback)->
|
||||
jobs = originalFolder.fileRefs.map (file)->
|
||||
return (cb)->
|
||||
projectEntityHandler.copyFileFromExistingProjectWithProject newProject, desFolder._id, originalProject_id, file, cb
|
||||
async.parallelLimit jobs, 5, callback
|
||||
|
||||
copyFolder = (folder, desFolder, callback)->
|
||||
jobs = folder.folders.map (childFolder)->
|
||||
return (callback)->
|
||||
projectEntityHandler.addFolder newProject, desFolder._id, childFolder.name, (err, newFolder)->
|
||||
copyFolder childFolder, newFolder, callback
|
||||
jobs.push (cb)->
|
||||
copyDocs folder, desFolder, cb
|
||||
jobs.push (cb)->
|
||||
copyFiles folder, desFolder, cb
|
||||
|
||||
async.series jobs, callback
|
||||
_copyFolderRecursivly: (newProject_id, originalProject_id, originalRootDoc, originalFolder, desFolder, docContents, callback)->
|
||||
ProjectGetter.getProject newProject_id, {rootFolder:true, name:true}, (err, newProject)->
|
||||
if err?
|
||||
logger.err project_id:newProject_id, "could not get project"
|
||||
return cb(err)
|
||||
|
||||
copyFolder originalProject.rootFolder[0], newProject.rootFolder[0], ->
|
||||
callback(err, newProject)
|
||||
jobs = originalFolder.folders.map (childFolder)->
|
||||
return (cb)->
|
||||
projectEntityHandler.addFolderWithProject newProject, desFolder?._id, childFolder.name, (err, newFolder)->
|
||||
return cb(err) if err?
|
||||
ProjectDuplicator._copyFolderRecursivly newProject_id, originalProject_id, originalRootDoc, childFolder, newFolder, docContents, cb
|
||||
|
||||
jobs.push (cb)->
|
||||
ProjectDuplicator._copyFiles newProject, originalProject_id, originalFolder, desFolder, cb
|
||||
jobs.push (cb)->
|
||||
ProjectDuplicator._copyDocs newProject, originalRootDoc, originalFolder, desFolder, docContents, cb
|
||||
|
||||
async.series jobs, callback
|
||||
|
||||
duplicate: (owner, originalProject_id, newProjectName, callback)->
|
||||
|
||||
jobs =
|
||||
flush: (cb)->
|
||||
DocumentUpdaterHandler.flushProjectToMongo originalProject_id, cb
|
||||
originalProject: (cb)->
|
||||
ProjectGetter.getProject originalProject_id, {compiler:true, rootFolder:true, rootDoc_id:true}, cb
|
||||
newProject: (cb)->
|
||||
projectCreationHandler.createBlankProject owner._id, newProjectName, cb
|
||||
originalRootDoc: (cb)->
|
||||
projectLocator.findRootDoc {project_id:originalProject_id}, cb
|
||||
docContentsArray: (cb)->
|
||||
DocstoreManager.getAllDocs originalProject_id, cb
|
||||
|
||||
async.series jobs, (err, results)->
|
||||
if err?
|
||||
logger.err err:err, originalProject_id:originalProject_id, "error duplicating project"
|
||||
return callback(err)
|
||||
{originalProject, newProject, originalRootDoc, docContentsArray} = results
|
||||
|
||||
originalRootDoc = originalRootDoc[0]
|
||||
|
||||
docContents = {}
|
||||
for docContent in docContentsArray
|
||||
docContents[docContent._id] = docContent
|
||||
|
||||
projectOptionsHandler.setCompiler newProject._id, originalProject.compiler, ->
|
||||
|
||||
ProjectDuplicator._copyFolderRecursivly newProject._id, originalProject_id, originalRootDoc, originalProject.rootFolder[0], newProject.rootFolder[0], docContents, ->
|
||||
if err?
|
||||
logger.err err:err, originalProject_id:originalProject_id, newProjectName:newProjectName, "error cloning project"
|
||||
callback(err, newProject)
|
||||
|
|
|
@ -26,6 +26,8 @@ module.exports = ProjectEditorHandler =
|
|||
dropbox:false
|
||||
compileTimeout: 60
|
||||
compileGroup:"standard"
|
||||
templates: false
|
||||
references: false
|
||||
|
||||
if project.owner_ref.features?
|
||||
if project.owner_ref.features.collaborators?
|
||||
|
@ -37,7 +39,11 @@ module.exports = ProjectEditorHandler =
|
|||
if project.owner_ref.features.compileTimeout?
|
||||
result.features.compileTimeout = project.owner_ref.features.compileTimeout
|
||||
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"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
Project = require('../../models/Project').Project
|
||||
settings = require "settings-sharelatex"
|
||||
Doc = require('../../models/Doc').Doc
|
||||
Folder = require('../../models/Folder').Folder
|
||||
File = require('../../models/File').File
|
||||
|
@ -75,23 +76,21 @@ module.exports = ProjectEntityHandler =
|
|||
documentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler')
|
||||
documentUpdaterHandler.flushProjectToMongo project_id, (error) ->
|
||||
return callback(error) if error?
|
||||
Project.findById project_id, (error, project) ->
|
||||
ProjectGetter.getProject project_id, {name:true}, (error, project) ->
|
||||
return callback(error) if error?
|
||||
requests = []
|
||||
self.getAllDocs project_id, (error, docs) ->
|
||||
return callback(error) if error?
|
||||
for docPath, doc of docs
|
||||
do (docPath, doc) ->
|
||||
requests.push (callback) ->
|
||||
tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0},
|
||||
callback
|
||||
requests.push (cb) ->
|
||||
tpdsUpdateSender.addDoc {project_id:project_id, doc_id:doc._id, path:docPath, project_name:project.name, rev:doc.rev||0}, cb
|
||||
self.getAllFiles project_id, (error, files) ->
|
||||
return callback(error) if error?
|
||||
for filePath, file of files
|
||||
do (filePath, file) ->
|
||||
requests.push (callback) ->
|
||||
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev},
|
||||
callback
|
||||
requests.push (cb) ->
|
||||
tpdsUpdateSender.addFile {project_id:project_id, file_id:file._id, path:filePath, project_name:project.name, rev:file.rev}, cb
|
||||
async.series requests, (err) ->
|
||||
logger.log project_id:project_id, "finished flushing project to tpds"
|
||||
callback(err)
|
||||
|
@ -110,27 +109,36 @@ module.exports = ProjectEntityHandler =
|
|||
options = {}
|
||||
DocstoreManager.getDoc project_id, doc_id, options, callback
|
||||
|
||||
addDoc: (project_or_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
|
||||
Project.getProject project_or_id, "", (err, project) ->
|
||||
logger.log project: project._id, folder_id: folder_id, doc_name: docName, "adding doc"
|
||||
return callback(err) if err?
|
||||
confirmFolder project, folder_id, (folder_id)=>
|
||||
doc = new Doc name: docName
|
||||
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
|
||||
# which hasn't been created in docstore.
|
||||
DocstoreManager.updateDoc project._id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
|
||||
return callback(err) if err?
|
||||
Project.putElement project._id, folder_id, doc, "doc", (err, result)=>
|
||||
return callback(err) if err?
|
||||
tpdsUpdateSender.addDoc {
|
||||
project_id: project._id,
|
||||
doc_id: doc._id
|
||||
path: result.path.fileSystem,
|
||||
project_name: project.name,
|
||||
rev: 0
|
||||
}, (err) ->
|
||||
return callback(err) if err?
|
||||
callback(null, doc, folder_id)
|
||||
|
||||
addDoc: (project_id, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
|
||||
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
|
||||
if err?
|
||||
logger.err project_id:project_id, err:err, "error getting project for add doc"
|
||||
return callback(err)
|
||||
ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, callback
|
||||
|
||||
addDocWithProject: (project, folder_id, docName, docLines, callback = (error, doc, folder_id) ->)=>
|
||||
project_id = project._id
|
||||
logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
|
||||
confirmFolder project, folder_id, (folder_id)=>
|
||||
doc = new Doc name: docName
|
||||
# Put doc in docstore first, so that if it errors, we don't have a doc_id in the project
|
||||
# which hasn't been created in docstore.
|
||||
DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, (err, modified, rev) ->
|
||||
return callback(err) if err?
|
||||
|
||||
ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=>
|
||||
return callback(err) if err?
|
||||
tpdsUpdateSender.addDoc {
|
||||
project_id: project_id,
|
||||
doc_id: doc?._id
|
||||
path: result?.path?.fileSystem,
|
||||
project_name: project.name,
|
||||
rev: 0
|
||||
}, (err) ->
|
||||
if err?
|
||||
logger.err err:err, "error adding doc to tpdsworker, contining anyway"
|
||||
callback(null, doc, folder_id)
|
||||
|
||||
restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
|
||||
# getDoc will return the deleted doc's lines, but we don't actually remove
|
||||
|
@ -139,22 +147,34 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(error) if error?
|
||||
ProjectEntityHandler.addDoc project_id, null, name, lines, callback
|
||||
|
||||
addFile: (project_or_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
|
||||
ProjectGetter.getProjectWithOnlyFolders project_or_id, (err, project) ->
|
||||
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
|
||||
return callback(err) if err?
|
||||
confirmFolder project, folder_id, (folder_id)->
|
||||
fileRef = new File name : fileName
|
||||
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
|
||||
return callback(err)
|
||||
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
|
||||
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, project_name:project.name, rev:fileRef.rev}, ->
|
||||
callback(err, fileRef, folder_id)
|
||||
addFile: (project_id, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
|
||||
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
|
||||
if err?
|
||||
logger.err project_id:project_id, err:err, "error getting project for add file"
|
||||
return callback(err)
|
||||
ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, callback
|
||||
|
||||
replaceFile: (project_or_id, file_id, fsPath, callback)->
|
||||
Project.getProject project_or_id, "", (err, project) ->
|
||||
addFileWithProject: (project, folder_id, fileName, path, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
|
||||
return callback(err) if err?
|
||||
confirmFolder project, folder_id, (folder_id)->
|
||||
fileRef = new File name : fileName
|
||||
FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
|
||||
return callback(err)
|
||||
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
|
||||
if err?
|
||||
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
|
||||
return callback(err)
|
||||
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error sending file to tpdsworker"
|
||||
callback(null, fileRef, folder_id)
|
||||
|
||||
replaceFile: (project_id, file_id, fsPath, callback)->
|
||||
ProjectGetter.getProject project_id, {name:true}, (err, project) ->
|
||||
return callback(err) if err?
|
||||
findOpts =
|
||||
project_id:project._id
|
||||
|
@ -182,21 +202,36 @@ module.exports = ProjectEntityHandler =
|
|||
Project.update conditons, update, {}, (err, second)->
|
||||
callback()
|
||||
|
||||
copyFileFromExistingProject: (project_or_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
|
||||
Project.getProject project_or_id, "", (err, project) ->
|
||||
logger.log project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
|
||||
return callback(err) if err?
|
||||
confirmFolder project, folder_id, (folder_id)=>
|
||||
if !origonalFileRef?
|
||||
logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null"
|
||||
return callback()
|
||||
fileRef = new File name : origonalFileRef.name
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
|
||||
copyFileFromExistingProject: (project_id, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
|
||||
logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3"
|
||||
ProjectGetter.getProject project_id, {name:true}, (err, project) ->
|
||||
if err?
|
||||
logger.err project_id:project_id, err:err, "error getting project for copy file from existing project"
|
||||
return callback(err)
|
||||
ProjectEntityHandler.copyFileFromExistingProjectWithProject project, folder_id, originalProject_id, origonalFileRef, callback
|
||||
|
||||
|
||||
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, callback = (error, fileRef, folder_id) ->)->
|
||||
project_id = project._id
|
||||
logger.log project_id:project_id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "copying file in s3 with project"
|
||||
return callback(err) if err?
|
||||
confirmFolder project, folder_id, (folder_id)=>
|
||||
if !origonalFileRef?
|
||||
logger.err project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "file trying to copy is null"
|
||||
return callback()
|
||||
fileRef = new File name : origonalFileRef.name
|
||||
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
|
||||
return callback(err)
|
||||
ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error coping file in s3"
|
||||
Project.putElement project._id, folder_id, fileRef, "file", (err, result)=>
|
||||
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result.path.fileSystem, rev:fileRef.rev, project_name:project.name}, (error) ->
|
||||
callback(error, fileRef, folder_id)
|
||||
logger.err err:err, project_id:project._id, folder_id:folder_id, "error putting element as part of copy"
|
||||
return callback(err)
|
||||
tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
|
||||
if err?
|
||||
logger.err err:err, project_id:project._id, folder_id:folder_id, originalProject_id:originalProject_id, origonalFileRef:origonalFileRef, "error sending file to tpds worker"
|
||||
callback(null, fileRef, folder_id)
|
||||
|
||||
mkdirp: (project_id, path, callback = (err, newlyCreatedFolders, lastFolderInPath)->)->
|
||||
self = @
|
||||
|
@ -204,7 +239,7 @@ module.exports = ProjectEntityHandler =
|
|||
folders = _.select folders, (folder)->
|
||||
return folder.length != 0
|
||||
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)=>
|
||||
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
|
||||
if path == '/'
|
||||
logger.log project_id: project._id, "mkdir is only trying to make path of / so sending back root folder"
|
||||
return callback(null, [], project.rootFolder[0])
|
||||
|
@ -217,7 +252,7 @@ module.exports = ProjectEntityHandler =
|
|||
if parentFolder?
|
||||
parentFolder_id = parentFolder._id
|
||||
builtUpPath = "#{builtUpPath}/#{folderName}"
|
||||
projectLocator.findElementByPath project_id, builtUpPath, (err, foundFolder)=>
|
||||
projectLocator.findElementByPath project, builtUpPath, (err, foundFolder)=>
|
||||
if !foundFolder?
|
||||
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
|
||||
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
|
||||
|
@ -235,16 +270,23 @@ module.exports = ProjectEntityHandler =
|
|||
folders = _.select folders, (folder)->
|
||||
!folder.filterOut
|
||||
callback(null, folders, lastFolder)
|
||||
|
||||
addFolder: (project_or_id, parentFolder_id, folderName, callback) ->
|
||||
folder = new Folder name: folderName
|
||||
Project.getProject project_or_id, "", (err, project) ->
|
||||
return callback(err) if err?
|
||||
confirmFolder project, parentFolder_id, (parentFolder_id)=>
|
||||
logger.log project: project_or_id, parentFolder_id:parentFolder_id, folderName:folderName, "new folder added"
|
||||
Project.putElement project._id, parentFolder_id, folder, "folder", (err, result)=>
|
||||
if callback?
|
||||
callback(err, folder, parentFolder_id)
|
||||
|
||||
addFolder: (project_id, parentFolder_id, folderName, callback) ->
|
||||
ProjectGetter.getProjectWithOnlyFolders project_id, (err, project)=>
|
||||
if err?
|
||||
logger.err project_id:project_id, err:err, "error getting project for add folder"
|
||||
return callback(err)
|
||||
ProjectEntityHandler.addFolderWithProject project, parentFolder_id, folderName, callback
|
||||
|
||||
addFolderWithProject: (project, parentFolder_id, folderName, callback = (err, folder, parentFolder_id)->) ->
|
||||
confirmFolder project, parentFolder_id, (parentFolder_id)=>
|
||||
folder = new Folder name: folderName
|
||||
logger.log project: project._id, parentFolder_id:parentFolder_id, folderName:folderName, "adding new folder"
|
||||
ProjectEntityHandler._putElement project, parentFolder_id, folder, "folder", (err, result)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project._id, "error adding folder to project"
|
||||
return callback(err)
|
||||
callback(err, folder, parentFolder_id)
|
||||
|
||||
updateDocLines : (project_id, doc_id, lines, callback = (error) ->)->
|
||||
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->
|
||||
|
@ -281,7 +323,7 @@ module.exports = ProjectEntityHandler =
|
|||
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
|
||||
return callback("No entityType set")
|
||||
entityType = entityType.toLowerCase()
|
||||
Project.findById project_id, (err, project)=>
|
||||
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
|
||||
return callback(err) if err?
|
||||
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path)->
|
||||
return callback(err) if err?
|
||||
|
@ -302,7 +344,7 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(error) if error?
|
||||
self._removeElementFromMongoArray Project, project_id, path.mongo, (err)->
|
||||
return callback(err) if err?
|
||||
Project.putElement project_id, destinationFolder_id, entity, entityType, (err, result)->
|
||||
ProjectEntityHandler._putElement project, destinationFolder_id, entity, entityType, (err, result)->
|
||||
return callback(err) if err?
|
||||
opts =
|
||||
project_id:project_id
|
||||
|
@ -319,7 +361,7 @@ module.exports = ProjectEntityHandler =
|
|||
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
|
||||
return callback("No entityType set")
|
||||
entityType = entityType.toLowerCase()
|
||||
Project.findById project_id, (err, project)=>
|
||||
ProjectGetter.getProject project_id, {name:true, rootFolder:true}, (err, project)=>
|
||||
return callback(error) if error?
|
||||
projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=>
|
||||
return callback(error) if error?
|
||||
|
@ -338,7 +380,7 @@ module.exports = ProjectEntityHandler =
|
|||
logger.err err: "No entityType set", project_id: project_id, entity_id: entity_id
|
||||
return callback("No entityType set")
|
||||
entityType = entityType.toLowerCase()
|
||||
Project.findById project_id, (err, project)=>
|
||||
ProjectGetter.getProject project_id, {rootFolder:true, name:true}, (err, project)=>
|
||||
projectLocator.findElement {project:project, element_id:entity_id, type:entityType}, (err, entity, path, folder)=>
|
||||
if err?
|
||||
return callback err
|
||||
|
@ -426,6 +468,72 @@ module.exports = ProjectEntityHandler =
|
|||
}
|
||||
}, {}, callback
|
||||
|
||||
|
||||
_countElements : (project, callback)->
|
||||
|
||||
countFolder = (folder, cb = (err, count)->)->
|
||||
|
||||
jobs = _.map folder?.folders, (folder)->
|
||||
(asyncCb)-> countFolder folder, asyncCb
|
||||
|
||||
async.series jobs, (err, subfolderCounts)->
|
||||
total = 0
|
||||
|
||||
if subfolderCounts?.length > 0
|
||||
total = _.reduce subfolderCounts, (a, b)-> return a + b
|
||||
if folder?.folders?.length?
|
||||
total += folder?.folders?.length
|
||||
if folder?.docs?.length?
|
||||
total += folder?.docs?.length
|
||||
if folder?.fileRefs?.length?
|
||||
total += folder?.fileRefs?.length
|
||||
cb(null, total)
|
||||
|
||||
countFolder project.rootFolder[0], callback
|
||||
|
||||
_putElement: (project, folder_id, element, type, callback = (err, path)->)->
|
||||
|
||||
sanitizeTypeOfElement = (elementType)->
|
||||
lastChar = elementType.slice -1
|
||||
if lastChar != "s"
|
||||
elementType +="s"
|
||||
if elementType == "files"
|
||||
elementType = "fileRefs"
|
||||
return elementType
|
||||
|
||||
if !element?
|
||||
e = new Error("no element passed to be inserted")
|
||||
logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null"
|
||||
return callback(e)
|
||||
type = sanitizeTypeOfElement type
|
||||
|
||||
if !folder_id?
|
||||
folder_id = project.rootFolder[0]._id
|
||||
ProjectEntityHandler._countElements project, (err, count)->
|
||||
if count > settings.maxEntitiesPerProject
|
||||
logger.warn project_id:project._id, "project too big, stopping insertions"
|
||||
return callback("project_has_to_many_files")
|
||||
projectLocator.findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=>
|
||||
if err?
|
||||
logger.err err:err, project_id:project._id, folder_id:folder_id, type:type, element:element, "error finding folder for _putElement"
|
||||
return callback(err)
|
||||
newPath =
|
||||
fileSystem: "#{path.fileSystem}/#{element.name}"
|
||||
mongo: path.mongo
|
||||
logger.log project_id: project._id, element_id: element._id, fileType: type, folder_id: folder_id, "adding element to project"
|
||||
id = element._id+''
|
||||
element._id = require('mongoose').Types.ObjectId(id)
|
||||
conditions = _id:project._id
|
||||
mongopath = "#{path.mongo}.#{type}"
|
||||
update = "$push":{}
|
||||
update["$push"][mongopath] = element
|
||||
Project.update conditions, update, {}, (err)->
|
||||
if err?
|
||||
logger.err err: err, project_id: project._id, 'error saving in putElement project'
|
||||
return callback(err)
|
||||
callback(err, {path:newPath})
|
||||
|
||||
|
||||
confirmFolder = (project, folder_id, callback)->
|
||||
logger.log folder_id:folder_id, project_id:project._id, "confirming folder in project"
|
||||
if folder_id+'' == 'undefined'
|
||||
|
|
|
@ -2,31 +2,54 @@ mongojs = require("../../infrastructure/mongojs")
|
|||
db = mongojs.db
|
||||
ObjectId = mongojs.ObjectId
|
||||
async = require "async"
|
||||
Errors = require("../../errors")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
|
||||
|
||||
module.exports = ProjectGetter =
|
||||
EXCLUDE_DEPTH: 8
|
||||
|
||||
|
||||
getProjectWithoutDocLines: (project_id, callback=(error, project) ->) ->
|
||||
excludes = {}
|
||||
for i in [1..@EXCLUDE_DEPTH]
|
||||
for i in [1..ProjectGetter.EXCLUDE_DEPTH]
|
||||
excludes["rootFolder#{Array(i).join(".folder")}.docs.lines"] = 0
|
||||
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
|
||||
callback error, projects[0]
|
||||
|
||||
getProjectWithOnlyFolders: (project_id, callback=(error, project) ->) ->
|
||||
excludes = {}
|
||||
for i in [1..@EXCLUDE_DEPTH]
|
||||
for i in [1..ProjectGetter.EXCLUDE_DEPTH]
|
||||
excludes["rootFolder#{Array(i).join(".folder")}.docs"] = 0
|
||||
excludes["rootFolder#{Array(i).join(".folder")}.fileRefs"] = 0
|
||||
db.projects.find _id: ObjectId(project_id.toString()), excludes, (error, projects = []) ->
|
||||
callback error, projects[0]
|
||||
|
||||
|
||||
getProject: (query, projection, callback = (error, project) ->) ->
|
||||
if !query?
|
||||
return callback("no query provided")
|
||||
|
||||
if typeof(projection) == "function"
|
||||
callback = projection
|
||||
|
||||
if typeof query == "string"
|
||||
query = _id: ObjectId(query)
|
||||
else if query instanceof ObjectId
|
||||
query = _id: query
|
||||
db.projects.findOne query, projection, callback
|
||||
else if query?.toString().length == 24 # sometimes mongoose ids are hard to identify, this will catch them
|
||||
query = _id: ObjectId(query.toString())
|
||||
else
|
||||
err = new Error("malformed get request")
|
||||
logger.log query:query, err:err, type:typeof(query), "malformed get request"
|
||||
return callback(err)
|
||||
|
||||
db.projects.find query, projection, (err, project)->
|
||||
if err?
|
||||
logger.err err:err, query:query, projection:projection, "error getting project"
|
||||
return callback(err)
|
||||
callback(null, project?[0])
|
||||
|
||||
populateProjectWithUsers: (project, callback=(error, project) ->) ->
|
||||
# eventually this should be in a UserGetter.getUser module
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
Project = require('../../models/Project').Project
|
||||
ProjectGetter = require("./ProjectGetter")
|
||||
Errors = require "../../errors"
|
||||
_ = require('underscore')
|
||||
logger = require('logger-sharelatex')
|
||||
async = require('async')
|
||||
|
||||
module.exports =
|
||||
findElement: (options, callback = (err, element, path, parentFolder)->)->
|
||||
module.exports = ProjectLocator =
|
||||
findElement: (options, _callback = (err, element, path, parentFolder)->)->
|
||||
callback = (args...) ->
|
||||
_callback(args...)
|
||||
_callback = () ->
|
||||
|
||||
{project, project_id, element_id, type} = options
|
||||
elementType = sanitizeTypeOfElement type
|
||||
|
||||
|
@ -46,7 +51,7 @@ module.exports =
|
|||
if project?
|
||||
startSearch(project)
|
||||
else
|
||||
Project.findById project_id, (err, project)->
|
||||
ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
|
||||
return callback(err) if err?
|
||||
if !project?
|
||||
return callback(new Errors.NotFoundError("project not found"))
|
||||
|
@ -62,8 +67,12 @@ module.exports =
|
|||
if project?
|
||||
getRootDoc project
|
||||
else
|
||||
Project.findById project_id, (err, project)->
|
||||
getRootDoc project
|
||||
ProjectGetter.getProject project_id, {rootFolder:true, rootDoc_id:true}, (err, project)->
|
||||
if err?
|
||||
logger.err err:err, "error getting project"
|
||||
return callback(err)
|
||||
else
|
||||
getRootDoc project
|
||||
|
||||
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
|
||||
|
||||
|
@ -122,11 +131,11 @@ module.exports =
|
|||
async.waterfall jobs, callback
|
||||
|
||||
findUsersProjectByName: (user_id, projectName, callback)->
|
||||
Project.findAllUsersProjects user_id, 'name', (err, projects, collabertions=[])->
|
||||
Project.findAllUsersProjects user_id, 'name archived', (err, projects, collabertions=[])->
|
||||
projects = projects.concat(collabertions)
|
||||
projectName = projectName.toLowerCase()
|
||||
project = _.find projects, (project)->
|
||||
project.name.toLowerCase() == projectName
|
||||
project = _.find projects, (project)->
|
||||
project.name.toLowerCase() == projectName and project.archived != true
|
||||
logger.log user_id:user_id, projectName:projectName, totalProjects:projects.length, project:project, "looking for project by name"
|
||||
callback(null, project)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
)
|
|
@ -15,7 +15,7 @@ module.exports = RateLimiterMiddlewear =
|
|||
###
|
||||
rateLimit: (opts) ->
|
||||
return (req, res, next) ->
|
||||
if req.session.user?
|
||||
if req.session?.user?
|
||||
user_id = req.session.user._id
|
||||
else
|
||||
user_id = req.ip
|
||||
|
|
|
@ -12,5 +12,4 @@ module.exports = SpellingController =
|
|||
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
|
||||
.on "error", (error) ->
|
||||
logger.error err: error, "Spelling API error"
|
||||
res.sendStatus 500
|
||||
.pipe(res)
|
||||
|
|
|
@ -13,6 +13,7 @@ module.exports =
|
|||
webRouter.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
|
||||
webRouter.get '/planned_maintenance', HomeController.externalPage("planned_maintenance", "Planned Maintenance")
|
||||
webRouter.get '/style', HomeController.externalPage("style_guide", "Style Guide")
|
||||
webRouter.get '/jobs', HomeController.externalPage("jobs", "Jobs")
|
||||
|
||||
webRouter.get '/dropbox', HomeController.externalPage("dropbox", "Dropbox and ShareLaTeX")
|
||||
|
||||
|
|
|
@ -52,11 +52,17 @@ module.exports =
|
|||
return callback(err) if err?
|
||||
callback err, subscriptions.length > 0, subscriptions
|
||||
|
||||
hasGroupMembersLimitReached: (user_id, callback)->
|
||||
hasGroupMembersLimitReached: (user_id, callback = (err, limitReached, subscription)->)->
|
||||
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting users subscription"
|
||||
return callback(err)
|
||||
if !subscription?
|
||||
logger.err user_id:user_id, "no subscription found for user"
|
||||
return callback("no subscription found")
|
||||
limitReached = subscription.member_ids.length >= subscription.membersLimit
|
||||
logger.log user_id:user_id, limitReached:limitReached, currentTotal: subscription.member_ids.length, membersLimit: subscription.membersLimit, "checking if subscription members limit has been reached"
|
||||
callback(err, limitReached)
|
||||
callback(err, limitReached, subscription)
|
||||
|
||||
getOwnerOfProject = (project_id, callback)->
|
||||
Project.findById project_id, 'owner_ref', (error, project) ->
|
||||
|
|
|
@ -247,6 +247,18 @@ module.exports = RecurlyWrapper =
|
|||
callback(error)
|
||||
)
|
||||
|
||||
extendTrial: (subscriptionId, daysUntilExpire = 7, callback)->
|
||||
next_renewal_date = new Date()
|
||||
next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
|
||||
logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
|
||||
@apiRequest({
|
||||
url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
|
||||
method : "put"
|
||||
}, (error, response, responseBody) =>
|
||||
if error?
|
||||
logger.err err:error, subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "error exending trial"
|
||||
callback(error)
|
||||
)
|
||||
|
||||
_parseSubscriptionXml: (xml, callback) ->
|
||||
@_parseXml xml, (error, data) ->
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
SecurityManager = require '../../managers/SecurityManager'
|
||||
SubscriptionHandler = require './SubscriptionHandler'
|
||||
PlansLocator = require("./PlansLocator")
|
||||
SubscriptionFormatters = require("./SubscriptionFormatters")
|
||||
SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder')
|
||||
LimitationsManager = require("./LimitationsManager")
|
||||
RecurlyWrapper = require './RecurlyWrapper'
|
||||
|
@ -9,7 +8,6 @@ Settings = require 'settings-sharelatex'
|
|||
logger = require('logger-sharelatex')
|
||||
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
|
||||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
require("../../infrastructure/Sixpack")
|
||||
|
||||
module.exports = SubscriptionController =
|
||||
|
||||
|
@ -98,14 +96,14 @@ module.exports = SubscriptionController =
|
|||
else
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groups) ->
|
||||
return next(error) if error?
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, "showing subscription dashboard"
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groups:groups, "showing subscription dashboard"
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
res.render "subscriptions/dashboard",
|
||||
title: "your_subscription"
|
||||
recomendedCurrency: subscription?.currency
|
||||
taxRate:subscription?.taxRate
|
||||
plans: plans
|
||||
subscription: subscription
|
||||
subscription: subscription || {}
|
||||
groups: groups
|
||||
subscriptionTabActive: true
|
||||
|
||||
|
@ -226,6 +224,14 @@ module.exports = SubscriptionController =
|
|||
else
|
||||
res.sendStatus 200
|
||||
|
||||
extendTrial: (req, res)->
|
||||
SecurityManager.getCurrentUser req, (error, user) ->
|
||||
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
|
||||
SubscriptionHandler.extendTrial subscription, 14, (err)->
|
||||
if err?
|
||||
res.send 500
|
||||
else
|
||||
res.send 200
|
||||
|
||||
recurlyNotificationParser: (req, res, next) ->
|
||||
xml = ""
|
||||
|
|
|
@ -1,30 +1,17 @@
|
|||
async = require("async")
|
||||
_ = require("underscore")
|
||||
settings = require("settings-sharelatex")
|
||||
SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
|
||||
_s = require("underscore.string")
|
||||
|
||||
module.exports = SubscriptionDomainHandler =
|
||||
|
||||
|
||||
getLicenceUserCanJoin: (user, callback)->
|
||||
getLicenceUserCanJoin: (user)->
|
||||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
if licence?
|
||||
callback null, licence
|
||||
else
|
||||
callback()
|
||||
|
||||
attemptToJoinGroup: (user, callback)->
|
||||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
if licence? and user.emailVerified
|
||||
SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, callback
|
||||
else
|
||||
callback "user not verified"
|
||||
return licence
|
||||
|
||||
rejectInvitationToGroup: (user, subscription, callback)->
|
||||
removeUserFromGroup(subscription.admin_id, user._id, callback)
|
||||
|
||||
|
||||
getDomainLicencePage: (user)->
|
||||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
if licence?.verifyEmail
|
||||
|
@ -32,20 +19,11 @@ module.exports = SubscriptionDomainHandler =
|
|||
else
|
||||
return undefined
|
||||
|
||||
|
||||
autoAllocate: (user, callback = ->)->
|
||||
licence = SubscriptionDomainHandler._findDomainLicence(user.email)
|
||||
#
|
||||
if licence?
|
||||
SubscriptionGroupHandler.addUserToGroup licence.adminUser_id, user.email, callback
|
||||
else
|
||||
callback()
|
||||
|
||||
|
||||
_findDomainLicence: (email)->
|
||||
licence = _.find settings.domainLicences, (licence)->
|
||||
_.find licence.domains, (domain)->
|
||||
_s.endsWith email, domain
|
||||
regex = "[@\.]#{domain}"
|
||||
return email.match(regex)
|
||||
|
||||
return licence
|
||||
|
||||
|
@ -53,4 +31,3 @@ module.exports = SubscriptionDomainHandler =
|
|||
licence = _.find settings.domainLicences, (licence)->
|
||||
licence?.subscription_id == subscription_id
|
||||
return licence
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
SubscriptionGroupHandler = require("./SubscriptionGroupHandler")
|
||||
logger = require("logger-sharelatex")
|
||||
SubscriptionLocator = require("./SubscriptionLocator")
|
||||
|
||||
ErrorsController = require("../Errors/ErrorController")
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
||||
_ = require("underscore")
|
||||
|
||||
|
@ -12,9 +9,12 @@ module.exports =
|
|||
|
||||
addUserToGroup: (req, res)->
|
||||
adminUserId = req.session.user._id
|
||||
newEmail = req.body.email
|
||||
newEmail = req.body?.email?.toLowerCase()?.trim()
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
|
||||
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
|
||||
if err?
|
||||
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
|
||||
return res.sendStatus 500
|
||||
result =
|
||||
user:user
|
||||
if err and err.limitReached
|
||||
|
@ -25,7 +25,20 @@ module.exports =
|
|||
adminUserId = req.session.user._id
|
||||
userToRemove_id = req.params.user_id
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, ->
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
removeSelfFromGroup: (req, res)->
|
||||
adminUserId = req.query.admin_user_id
|
||||
userToRemove_id = req.session.user._id
|
||||
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
|
||||
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
|
||||
if err?
|
||||
logger.err err:err, userToRemove_id:userToRemove_id, adminUserId:adminUserId, "error removing self from group"
|
||||
return res.sendStatus 500
|
||||
res.send()
|
||||
|
||||
renderSubscriptionGroupAdminPage: (req, res)->
|
||||
|
@ -70,13 +83,16 @@ module.exports =
|
|||
subscription_id = req.params.subscription_id
|
||||
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
|
||||
return ErrorsController.notFound(req, res)
|
||||
SubscriptionGroupHandler.processGroupVerification req.session.user.email, subscription_id, req.query.token, (err)->
|
||||
email = req?.session?.user?.email
|
||||
logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group"
|
||||
SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)->
|
||||
if err? and err == "token_not_found"
|
||||
res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
|
||||
return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
|
||||
else if err?
|
||||
res.sendStatus 500
|
||||
return res.sendStatus 500
|
||||
else
|
||||
res.redirect "/user/subscription/#{subscription_id}/group/successful-join"
|
||||
logger.log subscription_id:subscription_id, email:email, "user successful completed join of group subscription"
|
||||
return res.redirect "/user/subscription/#{subscription_id}/group/successful-join"
|
||||
|
||||
renderSuccessfulJoinPage: (req, res)->
|
||||
subscription_id = req.params.subscription_id
|
||||
|
|
|
@ -9,15 +9,32 @@ logger = require("logger-sharelatex")
|
|||
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
settings = require("settings-sharelatex")
|
||||
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
|
||||
|
||||
module.exports = SubscriptionGroupHandler =
|
||||
|
||||
addUserToGroup: (adminUser_id, newEmail, callback)->
|
||||
addUserToGroup: (adminUserId, newEmail, callback)->
|
||||
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group"
|
||||
UserCreator.getUserOrCreateHoldingAccount newEmail, (err, user)->
|
||||
LimitationsManager.hasGroupMembersLimitReached adminUser_id, (err, limitReached)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error creating user for holding account"
|
||||
return callback(err)
|
||||
if !user?
|
||||
msg = "no user returned whenc reating holidng account or getting user"
|
||||
logger.err adminUserId:adminUserId, newEmail:newEmail, msg
|
||||
return callback(msg)
|
||||
LimitationsManager.hasGroupMembersLimitReached adminUserId, (err, limitReached, subscription)->
|
||||
if err?
|
||||
logger.err err:err, adminUserId:adminUserId, newEmail:newEmail, "error checking if limit reached for group plan"
|
||||
return callback(err)
|
||||
if limitReached
|
||||
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
|
||||
return callback(limitReached:limitReached)
|
||||
SubscriptionUpdater.addUserToGroup adminUser_id, user._id, (err)->
|
||||
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
|
||||
if err?
|
||||
logger.err err:err, "error adding user to group"
|
||||
return callback(err)
|
||||
NotificationsBuilder.groupPlan(user, {subscription_id:subscription._id}).read()
|
||||
userViewModel = buildUserViewModel(user)
|
||||
callback(err, userViewModel)
|
||||
|
||||
|
@ -60,13 +77,20 @@ module.exports = SubscriptionGroupHandler =
|
|||
EmailHandler.sendEmail "completeJoinGroupAccount", opts, callback
|
||||
|
||||
processGroupVerification: (userEmail, subscription_id, token, callback)->
|
||||
logger.log userEmail:userEmail, subscription_id:subscription_id, "processing group verification for user"
|
||||
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, token_subscription_id)->
|
||||
|
||||
if err? or subscription_id != token_subscription_id
|
||||
logger.err userEmail:userEmail, token:token, "token value not found for processing group verification"
|
||||
return callback("token_not_found")
|
||||
SubscriptionLocator.getSubscription subscription_id, (err, subscription)->
|
||||
SubscriptionGroupHandler.addUserToGroup subscription.admin_id, userEmail, callback
|
||||
if err?
|
||||
logger.err err:err, subscription:subscription, userEmail:userEmail, subscription_id:subscription_id, "error getting subscription"
|
||||
return callback(err)
|
||||
if !subscription?
|
||||
logger.warn subscription_id:subscription_id, userEmail:userEmail, "no subscription found"
|
||||
return callback()
|
||||
SubscriptionGroupHandler.addUserToGroup subscription?.admin_id, userEmail, callback
|
||||
|
||||
|
||||
|
||||
buildUserViewModel = (user)->
|
||||
|
|
|
@ -74,5 +74,5 @@ module.exports =
|
|||
return callback("no user found")
|
||||
SubscriptionUpdater.syncSubscription recurlySubscription, user?._id, callback
|
||||
|
||||
|
||||
|
||||
extendTrial: (subscription, daysToExend, callback)->
|
||||
RecurlyWrapper.extendTrial subscription.recurlySubscription_id, daysToExend, callback
|
||||
|
|
|
@ -22,4 +22,4 @@ module.exports =
|
|||
Subscription.findOne _id:subscription_id, callback
|
||||
|
||||
getSubscriptionByMemberIdAndId: (user_id, subscription_id, callback)->
|
||||
Subscription.findOne member_ids: user_id, _id:subscription_id, callback
|
||||
Subscription.findOne member_ids: user_id, _id:subscription_id, {_id:1}, callback
|
||||
|
|
|
@ -24,6 +24,8 @@ module.exports =
|
|||
webRouter.post '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.addUserToGroup
|
||||
webRouter.get '/subscription/group/export', AuthenticationController.requireLogin(), SubscriptionGroupController.exportGroupCsv
|
||||
webRouter.delete '/subscription/group/user/:user_id', AuthenticationController.requireLogin(), SubscriptionGroupController.removeUserFromGroup
|
||||
webRouter.delete '/subscription/group/user', AuthenticationController.requireLogin(), SubscriptionGroupController.removeSelfFromGroup
|
||||
|
||||
|
||||
webRouter.get '/user/subscription/:subscription_id/group/invited', AuthenticationController.requireLogin(), SubscriptionGroupController.renderGroupInvitePage
|
||||
webRouter.post '/user/subscription/:subscription_id/group/begin-join', AuthenticationController.requireLogin(), SubscriptionGroupController.beginJoinGroup
|
||||
|
@ -39,6 +41,7 @@ module.exports =
|
|||
webRouter.post '/user/subscription/cancel', AuthenticationController.requireLogin(), SubscriptionController.cancelSubscription
|
||||
webRouter.post '/user/subscription/reactivate', AuthenticationController.requireLogin(), SubscriptionController.reactivateSubscription
|
||||
|
||||
webRouter.put '/user/subscription/extend', AuthenticationController.requireLogin(), SubscriptionController.extendTrial
|
||||
|
||||
webRouter.get "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.renderUpgradeToAnnualPlanPage
|
||||
webRouter.post "/user/subscription/upgrade-annual", AuthenticationController.requireLogin(), SubscriptionController.processUpgradeToAnnualPlan
|
||||
|
|
|
@ -32,6 +32,9 @@ module.exports =
|
|||
insertOperation =
|
||||
"$addToSet": {member_ids:user_id}
|
||||
Subscription.findAndModify searchOps, insertOperation, (err, subscription)->
|
||||
if err?
|
||||
logger.err err:err, searchOps:searchOps, insertOperation:insertOperation, "error findy and modify add user to group"
|
||||
return callback(err)
|
||||
UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback
|
||||
|
||||
removeUserFromGroup: (adminUser_id, user_id, callback)->
|
||||
|
@ -39,10 +42,12 @@ module.exports =
|
|||
admin_id: adminUser_id
|
||||
removeOperation =
|
||||
"$pull": {member_ids:user_id}
|
||||
Subscription.update searchOps, removeOperation, ->
|
||||
Subscription.update searchOps, removeOperation, (err)->
|
||||
if err?
|
||||
logger.err err:err, searchOps:searchOps, removeOperation:removeOperation, "error removing user from group"
|
||||
return callback(err)
|
||||
UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, callback
|
||||
|
||||
|
||||
_createNewSubscription: (adminUser_id, callback)->
|
||||
logger.log adminUser_id:adminUser_id, "creating new subscription"
|
||||
subscription = new Subscription(admin_id:adminUser_id)
|
||||
|
|
|
@ -27,6 +27,7 @@ module.exports =
|
|||
currency:recurlySubscription?.currency
|
||||
taxRate:parseFloat(recurlySubscription?.tax_rate?._)
|
||||
groupPlan: subscription.groupPlan
|
||||
trial_ends_at:recurlySubscription?.trial_ends_at
|
||||
}, memberSubscriptions
|
||||
else
|
||||
callback null, null, memberSubscriptions
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
Settings = require "settings-sharelatex"
|
||||
logger = require("logger-sharelatex")
|
||||
User = require('../../models/User').User
|
||||
PlansLocator = require("./PlansLocator")
|
||||
|
@ -9,7 +8,7 @@ module.exports =
|
|||
conditions = _id:user_id
|
||||
update = {}
|
||||
plan = PlansLocator.findLocalPlanInSettings(plan_code)
|
||||
logger.log user_id:user_id, plan:plan, plan_code:plan_code, "updating users features"
|
||||
logger.log user_id:user_id, features:plan.features, plan_code:plan_code, "updating users features"
|
||||
update["features.#{key}"] = value for key, value of plan.features
|
||||
User.update conditions, update, (err)->
|
||||
callback err, plan.features
|
||||
|
|
|
@ -2,20 +2,53 @@ TagsHandler = require("./TagsHandler")
|
|||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
processTagsUpdate: (req, res)->
|
||||
getAllTags: (req, res, next)->
|
||||
user_id = req.session.user._id
|
||||
project_id = req.params.project_id
|
||||
if req.body.deletedTag?
|
||||
tag = req.body.deletedTag
|
||||
TagsHandler.deleteTag user_id, project_id, tag, ->
|
||||
res.send()
|
||||
logger.log {user_id}, "getting tags"
|
||||
TagsHandler.getAllTags user_id, (error, allTags)->
|
||||
return next(error) if error?
|
||||
res.json(allTags)
|
||||
|
||||
createTag: (req, res, next) ->
|
||||
user_id = req.session.user._id
|
||||
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
|
||||
tag = req.body.tag
|
||||
TagsHandler.addTag user_id, project_id, tag, ->
|
||||
res.send()
|
||||
logger.log user_id:user_id, project_id:project_id, body:req.body, "processing tag update"
|
||||
|
||||
getAllTags: (req, res)->
|
||||
TagsHandler.getAllTags req.session.user._id, (err, allTags)->
|
||||
res.send(allTags)
|
||||
logger.log {user_id, tag_id, name}, "renaming tag"
|
||||
TagsHandler.renameTag user_id, tag_id, name, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(204).end()
|
||||
|
|
|
@ -3,61 +3,84 @@ settings = require("settings-sharelatex")
|
|||
request = require("request")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
oneSecond = 1000
|
||||
module.exports =
|
||||
|
||||
|
||||
deleteTag: (user_id, project_id, tag, callback)->
|
||||
uri = buildUri(user_id, project_id)
|
||||
opts =
|
||||
uri:uri
|
||||
json:
|
||||
name:tag
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, project_id:project_id, tag:tag, "send delete tag to tags api"
|
||||
request.del opts, callback
|
||||
|
||||
addTag: (user_id, project_id, tag, callback)->
|
||||
uri = buildUri(user_id, project_id)
|
||||
opts =
|
||||
uri:uri
|
||||
json:
|
||||
name:tag
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, project_id:project_id, tag:tag, "send add tag to tags api"
|
||||
request.post opts, callback
|
||||
|
||||
requestTags: (user_id, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.tags.url}/user/#{user_id}/tag"
|
||||
json: true
|
||||
timeout: 2000
|
||||
request.get opts, (err, res, body)->
|
||||
statusCode = if res? then res.statusCode else 500
|
||||
if err? or statusCode != 200
|
||||
e = new Error("something went wrong getting tags, #{err}, #{statusCode}")
|
||||
logger.err err:err
|
||||
callback(e, [])
|
||||
else
|
||||
callback(null, body)
|
||||
|
||||
TIMEOUT = 1000
|
||||
module.exports = TagsHandler =
|
||||
getAllTags: (user_id, callback)->
|
||||
@requestTags user_id, (err, allTags)=>
|
||||
@_requestTags user_id, (err, allTags)=>
|
||||
if !allTags?
|
||||
allTags = []
|
||||
@groupTagsByProject allTags, (err, groupedByProject)->
|
||||
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "getting all tags from tags api"
|
||||
@_groupTagsByProject allTags, (err, groupedByProject)->
|
||||
logger.log allTags:allTags, user_id:user_id, groupedByProject:groupedByProject, "got all tags from tags api"
|
||||
callback err, allTags, groupedByProject
|
||||
|
||||
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)->
|
||||
uri = buildUri(user_id, project_id)
|
||||
url = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
|
||||
opts =
|
||||
uri:"#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}"
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, project_id:project_id, "removing project_id from tags"
|
||||
request.del opts, callback
|
||||
url: url
|
||||
timeout:TIMEOUT
|
||||
request.del opts, (err, res, body) ->
|
||||
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 = {}
|
||||
_.each tags, (tag)->
|
||||
_.each tag.project_ids, (project_id)->
|
||||
|
@ -69,7 +92,3 @@ module.exports =
|
|||
delete clonedTag.project_ids
|
||||
result[project_id].push(clonedTag)
|
||||
callback null, result
|
||||
|
||||
|
||||
buildUri = (user_id, project_id)->
|
||||
uri = "#{settings.apis.tags.url}/user/#{user_id}/project/#{project_id}/tag"
|
||||
|
|
|
@ -45,7 +45,7 @@ module.exports =
|
|||
path = "/" + req.params[0] # UpdateMerger expects leading slash
|
||||
source = req.headers["x-sl-update-source"] or "unknown"
|
||||
logger.log project_id: project_id, path: path, source: source, "received project contents update"
|
||||
UpdateMerger.mergeUpdate project_id, path, req, source, (error) ->
|
||||
UpdateMerger.mergeUpdate null, project_id, path, req, source, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus(200)
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports =
|
|||
FileTypeManager.shouldIgnore path, (err, shouldIgnore)->
|
||||
if shouldIgnore
|
||||
return callback()
|
||||
updateMerger.mergeUpdate project._id, path, updateRequest, source, callback
|
||||
updateMerger.mergeUpdate user_id, project._id, path, updateRequest, source, callback
|
||||
|
||||
|
||||
deleteUpdate: (user_id, projectName, path, source, callback)->
|
||||
|
|
|
@ -42,6 +42,9 @@ module.exports = TpdsUpdateSender =
|
|||
|
||||
_addEntity: (options, callback = (err)->)->
|
||||
getProjectsUsersIds options.project_id, (err, user_id, allUserIds)->
|
||||
if err?
|
||||
logger.err err:err, options:options, "error getting projects user ids"
|
||||
return callback(err)
|
||||
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store"
|
||||
postOptions =
|
||||
method : "post"
|
||||
|
@ -53,6 +56,9 @@ module.exports = TpdsUpdateSender =
|
|||
title: "addFile"
|
||||
streamOrigin : options.streamOrigin
|
||||
TpdsUpdateSender._enqueue options.project_id, "pipeStreamFrom", postOptions, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "error sending file to third party data store queued up for processing"
|
||||
return callback(err)
|
||||
logger.log project_id: options.project_id, user_id:user_id, path: options.path, uri:options.uri, rev:options.rev, "sending file to third party data store queued up for processing"
|
||||
callback(err)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ uuid = require('node-uuid')
|
|||
fs = require('fs')
|
||||
|
||||
module.exports =
|
||||
mergeUpdate: (project_id, path, updateRequest, source, callback = (error) ->)->
|
||||
mergeUpdate: (user_id, project_id, path, updateRequest, source, callback = (error) ->)->
|
||||
self = @
|
||||
logger.log project_id:project_id, path:path, "merging update from tpds"
|
||||
projectLocator.findElementByPath project_id, path, (err, element)=>
|
||||
|
@ -30,7 +30,7 @@ module.exports =
|
|||
if isFile
|
||||
self.p.processFile project_id, elementId, fsPath, path, source, callback
|
||||
else
|
||||
self.p.processDoc project_id, elementId, fsPath, path, source, callback
|
||||
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
|
||||
|
||||
deleteUpdate: (project_id, path, source, callback)->
|
||||
projectLocator.findElementByPath project_id, path, (err, element)->
|
||||
|
@ -49,14 +49,14 @@ module.exports =
|
|||
|
||||
p:
|
||||
|
||||
processDoc: (project_id, doc_id, fsPath, path, source, callback)->
|
||||
processDoc: (project_id, doc_id, user_id, fsPath, path, source, callback)->
|
||||
readFileIntoTextArray fsPath, (err, docLines)->
|
||||
if err?
|
||||
logger.err project_id:project_id, doc_id:doc_id, fsPath:fsPath, "error reading file into text array for process doc update"
|
||||
return callback(err)
|
||||
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds"
|
||||
if doc_id?
|
||||
editorController.setDoc project_id, doc_id, docLines, source, (err)->
|
||||
editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)->
|
||||
callback()
|
||||
else
|
||||
setupNewEntity project_id, path, (err, folder, fileName)->
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
child = require "child_process"
|
||||
logger = require "logger-sharelatex"
|
||||
metrics = require "../../infrastructure/Metrics"
|
||||
fs = require "fs"
|
||||
Path = require "path"
|
||||
_ = require("underscore")
|
||||
|
||||
ONE_MEG = 1024 * 1024
|
||||
|
||||
module.exports = ArchiveManager =
|
||||
extractZipArchive: (source, destination, _callback = (err) ->) ->
|
||||
callback = (args...) ->
|
||||
_callback(args...)
|
||||
_callback = () ->
|
||||
|
||||
timer = new metrics.Timer("unzipDirectory")
|
||||
logger.log source: source, destination: destination, "unzipping file"
|
||||
|
||||
unzip = child.spawn("unzip", [source, "-d", destination])
|
||||
_isZipTooLarge: (source, callback = (err, isTooLarge)->)->
|
||||
callback = _.once callback
|
||||
|
||||
# don't remove this line, some zips need
|
||||
# us to listen on this for some unknow reason
|
||||
unzip = child.spawn("unzip", ["-l", source])
|
||||
|
||||
output = ""
|
||||
unzip.stdout.on "data", (d)->
|
||||
output += d
|
||||
|
||||
error = null
|
||||
unzip.stderr.on "data", (chunk) ->
|
||||
|
@ -29,9 +31,80 @@ module.exports = ArchiveManager =
|
|||
callback(err)
|
||||
|
||||
unzip.on "exit", () ->
|
||||
timer.done()
|
||||
if error?
|
||||
error = new Error(error)
|
||||
logger.error err:error, source: source, destination: destination, "error unzipping file"
|
||||
callback(error)
|
||||
logger.error err:error, source: source, destination: destination, "error checking zip size"
|
||||
|
||||
lines = output.split("\n")
|
||||
lastLine = lines[lines.length - 2]?.trim()
|
||||
totalSizeInBytes = lastLine?.split(" ")?[0]
|
||||
|
||||
totalSizeInBytes = parseInt(totalSizeInBytes)
|
||||
|
||||
if !totalSizeInBytes? or isNaN(totalSizeInBytes)
|
||||
logger.err source:source, "error getting bytes of zip"
|
||||
return callback(new Error("something went wrong"))
|
||||
|
||||
isTooLarge = totalSizeInBytes > (ONE_MEG * 300)
|
||||
|
||||
callback(error, isTooLarge)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
extractZipArchive: (source, destination, _callback = (err) ->) ->
|
||||
callback = (args...) ->
|
||||
_callback(args...)
|
||||
_callback = () ->
|
||||
|
||||
ArchiveManager._isZipTooLarge source, (err, isTooLarge)->
|
||||
if err?
|
||||
logger.err err:err, "error checking size of zip file"
|
||||
return callback(err)
|
||||
|
||||
if isTooLarge
|
||||
return callback(new Error("zip_too_large"))
|
||||
|
||||
|
||||
timer = new metrics.Timer("unzipDirectory")
|
||||
logger.log source: source, destination: destination, "unzipping file"
|
||||
|
||||
unzip = child.spawn("unzip", [source, "-d", destination])
|
||||
|
||||
# don't remove this line, some zips need
|
||||
# us to listen on this for some unknow reason
|
||||
unzip.stdout.on "data", (d)->
|
||||
|
||||
error = null
|
||||
unzip.stderr.on "data", (chunk) ->
|
||||
error ||= ""
|
||||
error += chunk
|
||||
|
||||
unzip.on "error", (err) ->
|
||||
logger.error {err, source, destination}, "unzip failed"
|
||||
if err.code == "ENOENT"
|
||||
logger.error "unzip command not found. Please check the unzip command is installed"
|
||||
callback(err)
|
||||
|
||||
unzip.on "exit", () ->
|
||||
timer.done()
|
||||
if error?
|
||||
error = new Error(error)
|
||||
logger.error err:error, source: source, destination: destination, "error unzipping file"
|
||||
callback(error)
|
||||
|
||||
findTopLevelDirectory: (directory, callback = (error, topLevelDir) ->) ->
|
||||
fs.readdir directory, (error, files) ->
|
||||
return callback(error) if error?
|
||||
if files.length == 1
|
||||
childPath = Path.join(directory, files[0])
|
||||
fs.stat childPath, (error, stat) ->
|
||||
return callback(error) if error?
|
||||
if stat.isDirectory()
|
||||
return callback(null, childPath)
|
||||
else
|
||||
return callback(null, directory)
|
||||
else
|
||||
return callback(null, directory)
|
||||
|
||||
|
|
|
@ -4,62 +4,108 @@ _ = require "underscore"
|
|||
FileTypeManager = require "./FileTypeManager"
|
||||
EditorController = require "../Editor/EditorController"
|
||||
ProjectLocator = require "../Project/ProjectLocator"
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports = FileSystemImportManager =
|
||||
addDoc: (project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
|
||||
fs.readFile path, "utf8", (error, content = "") ->
|
||||
return callback(error) if error?
|
||||
content = content.replace(/\r/g, "")
|
||||
lines = content.split("\n")
|
||||
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
|
||||
|
||||
addFile: (project_id, folder_id, name, path, replace, callback = (error, file)-> )->
|
||||
if replace
|
||||
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
|
||||
addDoc: (user_id, project_id, folder_id, name, path, replace, callback = (error, doc)-> )->
|
||||
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
|
||||
if !isSafe
|
||||
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add doc is from symlink, stopping process"
|
||||
return callback("path is symlink")
|
||||
fs.readFile path, "utf8", (error, content = "") ->
|
||||
return callback(error) if error?
|
||||
return callback(new Error("Couldn't find folder")) if !folder?
|
||||
existingFile = null
|
||||
for fileRef in folder.fileRefs
|
||||
if fileRef.name == name
|
||||
existingFile = fileRef
|
||||
break
|
||||
if existingFile?
|
||||
EditorController.replaceFile project_id, existingFile._id, path, "upload", callback
|
||||
else
|
||||
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
|
||||
else
|
||||
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
|
||||
|
||||
addFolder: (project_id, folder_id, name, path, replace, callback = (error)-> ) ->
|
||||
EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
|
||||
return callback(error) if error?
|
||||
@addFolderContents project_id, new_folder._id, path, replace, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, new_folder
|
||||
|
||||
addFolderContents: (project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
|
||||
fs.readdir folderPath, (error, entries = []) =>
|
||||
return callback(error) if error?
|
||||
jobs = _.map entries, (entry) =>
|
||||
(callback) =>
|
||||
FileTypeManager.shouldIgnore entry, (error, ignore) =>
|
||||
content = content.replace(/\r/g, "")
|
||||
lines = content.split("\n")
|
||||
if replace
|
||||
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
|
||||
return callback(error) if error?
|
||||
if !ignore
|
||||
@addEntity project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
|
||||
return callback(new Error("Couldn't find folder")) if !folder?
|
||||
existingDoc = null
|
||||
for doc in folder.docs
|
||||
if doc.name == name
|
||||
existingDoc = doc
|
||||
break
|
||||
if existingDoc?
|
||||
EditorController.setDoc project_id, existingDoc._id, user_id, lines, "upload", callback
|
||||
else
|
||||
callback()
|
||||
async.parallelLimit jobs, 5, callback
|
||||
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
|
||||
else
|
||||
EditorController.addDocWithoutLock project_id, folder_id, name, lines, "upload", callback
|
||||
|
||||
addEntity: (project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
|
||||
FileTypeManager.isDirectory path, (error, isDirectory) =>
|
||||
return callback(error) if error?
|
||||
if isDirectory
|
||||
@addFolder project_id, folder_id, name, path, replace, callback
|
||||
addFile: (user_id, project_id, folder_id, name, path, replace, callback = (error, file)-> )->
|
||||
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
|
||||
if !isSafe
|
||||
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, name:name, path:path, "add file is from symlink, stopping insert"
|
||||
return callback("path is symlink")
|
||||
|
||||
if !replace
|
||||
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
|
||||
else
|
||||
FileTypeManager.isBinary name, path, (error, isBinary) =>
|
||||
ProjectLocator.findElement project_id: project_id, element_id: folder_id, type: "folder", (error, folder) ->
|
||||
return callback(error) if error?
|
||||
if isBinary
|
||||
@addFile project_id, folder_id, name, path, replace, callback
|
||||
return callback(new Error("Couldn't find folder")) if !folder?
|
||||
existingFile = null
|
||||
for fileRef in folder.fileRefs
|
||||
if fileRef.name == name
|
||||
existingFile = fileRef
|
||||
break
|
||||
if existingFile?
|
||||
EditorController.replaceFile project_id, existingFile._id, path, "upload", callback
|
||||
else
|
||||
@addDoc project_id, folder_id, name, path, replace, callback
|
||||
EditorController.addFileWithoutLock project_id, folder_id, name, path, "upload", callback
|
||||
|
||||
addFolder: (user_id, project_id, folder_id, name, path, replace, callback = (error)-> ) ->
|
||||
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
|
||||
if !isSafe
|
||||
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add folder is from symlink, stopping insert"
|
||||
return callback("path is symlink")
|
||||
EditorController.addFolderWithoutLock project_id, folder_id, name, "upload", (error, new_folder) =>
|
||||
return callback(error) if error?
|
||||
FileSystemImportManager.addFolderContents user_id, project_id, new_folder._id, path, replace, (error) ->
|
||||
return callback(error) if error?
|
||||
callback null, new_folder
|
||||
|
||||
addFolderContents: (user_id, project_id, parent_folder_id, folderPath, replace, callback = (error)-> ) ->
|
||||
FileSystemImportManager._isSafeOnFileSystem folderPath, (err, isSafe)->
|
||||
if !isSafe
|
||||
logger.log user_id:user_id, project_id:project_id, parent_folder_id:parent_folder_id, folderPath:folderPath, "add folder contents is from symlink, stopping insert"
|
||||
return callback("path is symlink")
|
||||
fs.readdir folderPath, (error, entries = []) =>
|
||||
return callback(error) if error?
|
||||
jobs = _.map entries, (entry) =>
|
||||
(callback) =>
|
||||
FileTypeManager.shouldIgnore entry, (error, ignore) =>
|
||||
return callback(error) if error?
|
||||
if !ignore
|
||||
FileSystemImportManager.addEntity user_id, project_id, parent_folder_id, entry, "#{folderPath}/#{entry}", replace, callback
|
||||
else
|
||||
callback()
|
||||
async.parallelLimit jobs, 5, callback
|
||||
|
||||
addEntity: (user_id, project_id, folder_id, name, path, replace, callback = (error, entity)-> ) ->
|
||||
FileSystemImportManager._isSafeOnFileSystem path, (err, isSafe)->
|
||||
if !isSafe
|
||||
logger.log user_id:user_id, project_id:project_id, folder_id:folder_id, path:path, "add entry is from symlink, stopping insert"
|
||||
return callback("path is symlink")
|
||||
|
||||
FileTypeManager.isDirectory path, (error, isDirectory) =>
|
||||
return callback(error) if error?
|
||||
if isDirectory
|
||||
FileSystemImportManager.addFolder user_id, project_id, folder_id, name, path, replace, callback
|
||||
else
|
||||
FileTypeManager.isBinary name, path, (error, isBinary) =>
|
||||
return callback(error) if error?
|
||||
if isBinary
|
||||
FileSystemImportManager.addFile user_id, project_id, folder_id, name, path, replace, callback
|
||||
else
|
||||
FileSystemImportManager.addDoc user_id, project_id, folder_id, name, path, replace, callback
|
||||
|
||||
|
||||
_isSafeOnFileSystem: (path, callback = (err, isSafe)->)->
|
||||
fs.lstat path, (err, stat)->
|
||||
if err?
|
||||
logger.err err:err, "error with path symlink check"
|
||||
return callback(err)
|
||||
isSafe = stat.isFile() or stat.isDirectory()
|
||||
callback(err, isSafe)
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@ module.exports = ProjectUploadController =
|
|||
if !name? or name.length == 0 or name.length > 150
|
||||
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
|
||||
return res.send success: false
|
||||
FileSystemImportManager.addEntity project_id, folder_id, name, path, true, (error, entity) ->
|
||||
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
|
||||
user_id = req.session.user._id
|
||||
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
|
||||
fs.unlink path, ->
|
||||
timer.done()
|
||||
if error?
|
||||
|
|
|
@ -9,19 +9,21 @@ module.exports = ProjectUploadHandler =
|
|||
createProjectFromZipArchive: (owner_id, name, zipPath, callback = (error, project) ->) ->
|
||||
ProjectCreationHandler.createBlankProject owner_id, name, (error, project) =>
|
||||
return callback(error) if error?
|
||||
@insertZipArchiveIntoFolder project._id, project.rootFolder[0]._id, zipPath, (error) ->
|
||||
@insertZipArchiveIntoFolder owner_id, project._id, project.rootFolder[0]._id, zipPath, (error) ->
|
||||
return callback(error) if error?
|
||||
ProjectRootDocManager.setRootDocAutomatically project._id, (error) ->
|
||||
return callback(error) if error?
|
||||
callback(error, project)
|
||||
|
||||
insertZipArchiveIntoFolder: (project_id, folder_id, path, callback = (error) ->) ->
|
||||
insertZipArchiveIntoFolder: (owner_id, project_id, folder_id, path, callback = (error) ->) ->
|
||||
destination = @_getDestinationDirectory path
|
||||
ArchiveManager.extractZipArchive path, destination, (error) ->
|
||||
return callback(error) if error?
|
||||
FileSystemImportManager.addFolderContents project_id, folder_id, destination, false, (error) ->
|
||||
ArchiveManager.findTopLevelDirectory destination, (error, topLevelDestination) ->
|
||||
return callback(error) if error?
|
||||
rimraf(destination, callback)
|
||||
FileSystemImportManager.addFolderContents owner_id, project_id, folder_id, topLevelDestination, false, (error) ->
|
||||
return callback(error) if error?
|
||||
rimraf(destination, callback)
|
||||
|
||||
_getDestinationDirectory: (source) ->
|
||||
return path.join(path.dirname(source), "#{path.basename(source, ".zip")}-#{Date.now()}")
|
||||
|
|
|
@ -13,8 +13,8 @@ module.exports =
|
|||
RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "file-upload"
|
||||
params: ["Project_id"]
|
||||
maxRequests: 100
|
||||
timeInterval: 60 * 20
|
||||
maxRequests: 200
|
||||
timeInterval: 60 * 30
|
||||
}),
|
||||
SecurityManager.requestCanModifyProject,
|
||||
ProjectUploadController.uploadFile
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
UserHandler = require("./UserHandler")
|
||||
UserDeleter = require("./UserDeleter")
|
||||
UserLocator = require("./UserLocator")
|
||||
User = require("../../models/User").User
|
||||
|
@ -8,12 +9,9 @@ metrics = require("../../infrastructure/Metrics")
|
|||
Url = require("url")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
UserUpdater = require("./UserUpdater")
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
|
||||
settings = require "settings-sharelatex"
|
||||
crypto = require "crypto"
|
||||
|
||||
module.exports =
|
||||
module.exports = UserController =
|
||||
|
||||
deleteUser: (req, res)->
|
||||
user_id = req.session.user._id
|
||||
|
@ -66,11 +64,18 @@ module.exports =
|
|||
if err?
|
||||
logger.err err:err, user_id:user_id, newEmail:newEmail, "problem updaing users email address"
|
||||
if err.message == "alread_exists"
|
||||
message = req.i18n.translate("alread_exists")
|
||||
message = req.i18n.translate("email_already_registered")
|
||||
else
|
||||
message = req.i18n.translate("problem_changing_email_address")
|
||||
return res.send 500, {message:message}
|
||||
res.sendStatus(200)
|
||||
User.findById user_id, (err, user)->
|
||||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting user for email update"
|
||||
return res.send 500
|
||||
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
|
||||
if err?
|
||||
logger.err err:err, "error populateGroupLicenceInvite"
|
||||
res.sendStatus(200)
|
||||
|
||||
logout : (req, res)->
|
||||
metrics.inc "user.logout"
|
||||
|
@ -85,32 +90,12 @@ module.exports =
|
|||
if !email? or email == ""
|
||||
res.sendStatus 422 # Unprocessable Entity
|
||||
return
|
||||
logger.log {email}, "registering new user"
|
||||
UserRegistrationHandler.registerNewUser {
|
||||
email: email
|
||||
password: crypto.randomBytes(32).toString("hex")
|
||||
}, (err, user)->
|
||||
if err? and err?.message != "EmailAlreadyRegistered"
|
||||
return next(err)
|
||||
|
||||
if err?.message == "EmailAlreadyRegistered"
|
||||
logger.log {email}, "user already exists, resending welcome email"
|
||||
|
||||
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||
return next(err) if err?
|
||||
|
||||
setNewPasswordUrl = "#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}&email=#{encodeURIComponent(email)}"
|
||||
|
||||
EmailHandler.sendEmail "registered", {
|
||||
to: user.email
|
||||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}, () ->
|
||||
|
||||
res.json {
|
||||
email: user.email
|
||||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}
|
||||
UserRegistrationHandler.registerNewUserAndSendActivationEmail email, (error, user, setNewPasswordUrl) ->
|
||||
return next(error) if error?
|
||||
res.json {
|
||||
email: user.email
|
||||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}
|
||||
|
||||
changePassword : (req, res, next = (error) ->)->
|
||||
metrics.inc "user.password-change"
|
||||
|
|
26
services/web/app/coffee/Features/User/UserHandler.coffee
Normal file
26
services/web/app/coffee/Features/User/UserHandler.coffee
Normal 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
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
UserLocator = require("./UserLocator")
|
||||
UserGetter = require("./UserGetter")
|
||||
ErrorController = require("../Errors/ErrorController")
|
||||
logger = require("logger-sharelatex")
|
||||
Settings = require("settings-sharelatex")
|
||||
fs = require('fs')
|
||||
|
@ -20,11 +22,33 @@ module.exports =
|
|||
sharedProjectData: sharedProjectData
|
||||
newTemplateData: newTemplateData
|
||||
new_email:req.query.new_email || ""
|
||||
|
||||
activateAccountPage: (req, res) ->
|
||||
# An 'activation' is actually just a password reset on an account that
|
||||
# was set with a random password originally.
|
||||
if !req.query?.user_id? or !req.query?.token?
|
||||
return ErrorController.notFound(req, res)
|
||||
|
||||
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
|
||||
return next(error) if error?
|
||||
if !user
|
||||
return ErrorController.notFound(req, res)
|
||||
if user.loginCount > 0
|
||||
# Already seen this user, so account must be activate
|
||||
# This lets users keep clicking the 'activate' link in their email
|
||||
# as a way to log in which, if I know our users, they will.
|
||||
res.redirect "/login?email=#{encodeURIComponent(user.email)}"
|
||||
else
|
||||
res.render 'user/activate',
|
||||
title: 'activate_account'
|
||||
email: user.email,
|
||||
token: req.query.token
|
||||
|
||||
loginPage : (req, res)->
|
||||
res.render 'user/login',
|
||||
title: 'login',
|
||||
redir: req.query.redir
|
||||
redir: req.query.redir,
|
||||
email: req.query.email
|
||||
|
||||
settingsPage : (req, res, next)->
|
||||
logger.log user: req.session.user, "loading settings page"
|
||||
|
|
|
@ -5,8 +5,12 @@ AuthenticationManager = require("../Authentication/AuthenticationManager")
|
|||
NewsLetterManager = require("../Newsletter/NewsletterManager")
|
||||
async = require("async")
|
||||
logger = require("logger-sharelatex")
|
||||
crypto = require("crypto")
|
||||
EmailHandler = require("../Email/EmailHandler")
|
||||
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
module.exports =
|
||||
module.exports = UserRegistrationHandler =
|
||||
validateEmail : (email) ->
|
||||
re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\ ".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA -Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return re.test(email)
|
||||
|
@ -58,6 +62,31 @@ module.exports =
|
|||
], (err)->
|
||||
logger.log user: user, "registered"
|
||||
callback(err, user)
|
||||
|
||||
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
|
||||
logger.log {email}, "registering new user"
|
||||
UserRegistrationHandler.registerNewUser {
|
||||
email: email
|
||||
password: crypto.randomBytes(32).toString("hex")
|
||||
}, (err, user)->
|
||||
if err? and err?.message != "EmailAlreadyRegistered"
|
||||
return callback(err)
|
||||
|
||||
if err?.message == "EmailAlreadyRegistered"
|
||||
logger.log {email}, "user already exists, resending welcome email"
|
||||
|
||||
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
|
||||
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
|
||||
return callback(err) if err?
|
||||
|
||||
setNewPasswordUrl = "#{settings.siteUrl}/user/activate?token=#{token}&user_id=#{user._id}"
|
||||
|
||||
EmailHandler.sendEmail "registered", {
|
||||
to: user.email
|
||||
setNewPasswordUrl: setNewPasswordUrl
|
||||
}, () ->
|
||||
|
||||
callback null, user, setNewPasswordUrl
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ for path in [
|
|||
"#{jsPath}main.js",
|
||||
"#{jsPath}libs.js",
|
||||
"#{jsPath}ace/ace.js",
|
||||
"#{jsPath}libs/pdfjs-1.0.1040/pdf.js",
|
||||
"#{jsPath}libs/pdfjs-1.0.1040/pdf.worker.js",
|
||||
"#{jsPath}libs/pdfjs-1.0.1040/compatibility.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/pdf.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
|
||||
"#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
|
||||
"/stylesheets/style.css"
|
||||
]
|
||||
filePath = Path.join __dirname, "../../../", "public#{path}"
|
||||
|
|
|
@ -31,6 +31,7 @@ ProjectSchema = new Schema
|
|||
description : {type:String, default:''}
|
||||
archived : { type: Boolean }
|
||||
deletedDocs : [DeletedDocSchema]
|
||||
imageName : { type: String }
|
||||
|
||||
ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
|
||||
if project_or_id._id?
|
||||
|
@ -65,51 +66,7 @@ ProjectSchema.statics.findAllUsersProjects = (user_id, requiredFields, callback)
|
|||
this.find {readOnly_refs:user_id}, requiredFields, (err, readOnlyProjects)=>
|
||||
callback(err, projects, collabertions, readOnlyProjects)
|
||||
|
||||
sanitizeTypeOfElement = (elementType)->
|
||||
lastChar = elementType.slice -1
|
||||
if lastChar != "s"
|
||||
elementType +="s"
|
||||
if elementType == "files"
|
||||
elementType = "fileRefs"
|
||||
return elementType
|
||||
|
||||
ProjectSchema.statics.putElement = (project_id, folder_id, element, type, callback)->
|
||||
if !element?
|
||||
e = new Error("no element passed to be inserted")
|
||||
logger.err project_id:project_id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as it was null"
|
||||
return callback(e)
|
||||
type = sanitizeTypeOfElement type
|
||||
this.findById project_id, (err, project)=>
|
||||
if err?
|
||||
callback(err)
|
||||
if !folder_id?
|
||||
folder_id = project.rootFolder[0]._id
|
||||
require('../Features/Project/ProjectLocator').findElement {project:project, element_id:folder_id, type:"folders"}, (err, folder, path)=>
|
||||
newPath =
|
||||
fileSystem: "#{path.fileSystem}/#{element.name}"
|
||||
mongo: path.mongo # TODO: This is not correct
|
||||
if err?
|
||||
callback(err)
|
||||
logger.log project_id: project_id, element_id: element._id, fileType: type, folder_id: folder_id, "adding element to project"
|
||||
id = element._id+''
|
||||
element._id = concreteObjectId(id)
|
||||
conditions = _id:project_id
|
||||
mongopath = "#{path.mongo}.#{type}"
|
||||
update = "$push":{}
|
||||
update["$push"][mongopath] = element
|
||||
this.update conditions, update, {}, (err)->
|
||||
if(err)
|
||||
logger.err err: err, project: project, 'error saving in putElement project'
|
||||
if callback?
|
||||
callback(err, {path:newPath})
|
||||
|
||||
getIndexOf = (searchEntity, id)->
|
||||
length = searchEntity.length
|
||||
count = 0
|
||||
while(count < length)
|
||||
if searchEntity[count]._id+"" == id+""
|
||||
return count
|
||||
count++
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ UserSchema = new Schema
|
|||
github: { type:Boolean, default: Settings.defaultFeatures.github }
|
||||
compileTimeout: { type:Number, default: Settings.defaultFeatures.compileTimeout }
|
||||
compileGroup: { type:String, default: Settings.defaultFeatures.compileGroup }
|
||||
templates: { type:Boolean, default: Settings.defaultFeatures.templates }
|
||||
references: { type:Boolean, default: Settings.defaultFeatures.references }
|
||||
}
|
||||
featureSwitches : {
|
||||
pdfng: { type: Boolean }
|
||||
|
|
|
@ -16,6 +16,7 @@ ReferalController = require('./Features/Referal/ReferalController')
|
|||
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
|
||||
AuthenticationController = require('./Features/Authentication/AuthenticationController')
|
||||
TagsController = require("./Features/Tags/TagsController")
|
||||
NotificationsController = require("./Features/Notifications/NotificationsController")
|
||||
CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter')
|
||||
UserInfoController = require('./Features/User/UserInfoController')
|
||||
UserController = require("./Features/User/UserController")
|
||||
|
@ -37,6 +38,7 @@ RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
|
|||
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
|
||||
InactiveProjectController = require("./Features/InactiveData/InactiveProjectController")
|
||||
ContactRouter = require("./Features/Contacts/ContactRouter")
|
||||
ReferencesController = require('./Features/References/ReferencesController')
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -46,7 +48,7 @@ module.exports = class Router
|
|||
if !Settings.allowPublicAccess
|
||||
webRouter.all '*', AuthenticationController.requireGlobalLogin
|
||||
|
||||
|
||||
|
||||
webRouter.get '/login', UserPagesController.loginPage
|
||||
AuthenticationController.addEndpointToLoginWhitelist '/login'
|
||||
|
||||
|
@ -67,16 +69,18 @@ module.exports = class Router
|
|||
StaticPagesRouter.apply(webRouter, apiRouter)
|
||||
RealTimeProxyRouter.apply(webRouter, apiRouter)
|
||||
ContactRouter.apply(webRouter, apiRouter)
|
||||
|
||||
|
||||
Modules.applyRouter(webRouter, apiRouter)
|
||||
|
||||
|
||||
if Settings.enableSubscriptions
|
||||
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
|
||||
|
||||
|
||||
webRouter.get '/blog', BlogController.getIndexPage
|
||||
webRouter.get '/blog/*', BlogController.getPage
|
||||
|
||||
|
||||
webRouter.get '/user/activate', UserPagesController.activateAccountPage
|
||||
|
||||
webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
|
||||
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
|
||||
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
|
||||
|
@ -128,8 +132,15 @@ module.exports = class Router
|
|||
webRouter.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
|
||||
webRouter.get '/project/download/zip', SecurityManager.requestCanAccessMultipleProjects, ProjectDownloadsController.downloadMultipleProjects
|
||||
|
||||
webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
|
||||
webRouter.post '/project/:project_id/tag', AuthenticationController.requireLogin(), TagsController.processTagsUpdate
|
||||
webRouter.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
|
||||
webRouter.post '/tag', AuthenticationController.requireLogin(), TagsController.createTag
|
||||
webRouter.post '/tag/:tag_id/rename', AuthenticationController.requireLogin(), TagsController.renameTag
|
||||
webRouter.delete '/tag/:tag_id', AuthenticationController.requireLogin(), TagsController.deleteTag
|
||||
webRouter.post '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.addProjectToTag
|
||||
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
|
||||
|
||||
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
|
||||
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
|
||||
|
||||
# Deprecated in favour of /internal/project/:project_id but still used by versioning
|
||||
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
|
||||
|
@ -156,7 +167,7 @@ module.exports = class Router
|
|||
|
||||
apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate
|
||||
apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate
|
||||
|
||||
|
||||
apiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents
|
||||
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.post "/project/:Project_id/messages", SecurityManager.requestCanAccessProject, ChatController.sendMessage
|
||||
|
||||
|
||||
webRouter.get /learn(\/.*)?/, WikiController.getPage
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", SecurityManager.requestCanAccessProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", SecurityManager.requestCanAccessProject, ReferencesController.indexAll
|
||||
|
||||
#Admin Stuff
|
||||
webRouter.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
|
||||
webRouter.get '/admin/user', SecurityManager.requestIsAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon
|
||||
|
@ -186,7 +200,7 @@ module.exports = class Router
|
|||
|
||||
apiRouter.get '/status', (req,res)->
|
||||
res.send("websharelatex is up")
|
||||
|
||||
|
||||
|
||||
webRouter.get '/health_check', HealthCheckController.check
|
||||
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"
|
||||
res.sendStatus(204)
|
||||
|
||||
webRouter.get '*', ErrorController.notFound
|
||||
webRouter.get '*', ErrorController.notFound
|
|
@ -53,7 +53,13 @@ block content
|
|||
|
||||
include ./editor/share
|
||||
|
||||
#ide-body(ng-cloak, layout="main", ng-hide="state.loading", resize-on="layout:chat:resize")
|
||||
#ide-body(
|
||||
ng-cloak,
|
||||
layout="main",
|
||||
ng-hide="state.loading",
|
||||
resize-on="layout:chat:resize",
|
||||
minimum-restore-size-west="130"
|
||||
)
|
||||
.ui-layout-west
|
||||
include ./editor/file-tree
|
||||
|
||||
|
@ -76,7 +82,7 @@ block content
|
|||
ng-click="done()"
|
||||
) ×
|
||||
h3 {{ title }}
|
||||
.modal-body {{ message }}
|
||||
.modal-body(ng-bind-html="message")
|
||||
.modal-footer
|
||||
button.btn.btn-info(ng-click="done()") #{translate("ok")}
|
||||
|
||||
|
@ -95,13 +101,13 @@ block content
|
|||
"paths" : {
|
||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
|
||||
"moment": "libs/moment-2.7.0",
|
||||
"libs/pdf": "libs/pdfjs-1.0.1040/pdf"
|
||||
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
|
||||
},
|
||||
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
|
||||
"waitSeconds": 0,
|
||||
"shim": {
|
||||
"libs/pdf": {
|
||||
deps: ["libs/pdfjs-1.0.1040/compatibility"]
|
||||
deps: ["libs/pdfjs-1.3.91/compatibility"]
|
||||
},
|
||||
"ace/ext-searchbox": {
|
||||
deps: ["ace/ace"]
|
||||
|
@ -120,7 +126,7 @@ block content
|
|||
|
||||
- locals.suppressDefaultJs = true
|
||||
|
||||
- var pdfPath = 'libs/pdfjs-1.0.1040/pdf.worker.js'
|
||||
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
|
||||
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
|
||||
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
|
||||
script(type='text/javascript').
|
||||
|
|
|
@ -6,6 +6,7 @@ div.full-size(
|
|||
resize-on="layout:main:resize"
|
||||
resize-proportionally="true"
|
||||
initial-size-east="'50%'"
|
||||
minimum-restore-size-east="300"
|
||||
)
|
||||
.ui-layout-center
|
||||
.loading-panel(ng-show="!editor.sharejs_doc || editor.opening")
|
||||
|
@ -19,6 +20,7 @@ div.full-size(
|
|||
keybindings="settings.mode",
|
||||
font-size="settings.fontSize",
|
||||
auto-complete="settings.autoComplete",
|
||||
spell-check="true",
|
||||
spell-check-language="project.spellCheckLanguage",
|
||||
highlights="onlineUserCursorHighlights[editor.open_doc_id]"
|
||||
show-print-margin="false",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
aside#file-tree(ng-controller="FileTreeController").full-size
|
||||
aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected': multiSelectedCount > 0 }").full-size
|
||||
.toolbar.toolbar-small.toolbar-alt(ng-if="permissions.write")
|
||||
a(
|
||||
href,
|
||||
|
@ -27,7 +27,8 @@ aside#file-tree(ng-controller="FileTreeController").full-size
|
|||
href,
|
||||
ng-click="startRenamingSelected()",
|
||||
tooltip="#{translate('rename')}",
|
||||
tooltip-placement="bottom"
|
||||
tooltip-placement="bottom",
|
||||
ng-show="multiSelectedCount == 0"
|
||||
)
|
||||
i.fa.fa-pencil
|
||||
a(
|
||||
|
@ -45,26 +46,24 @@ aside#file-tree(ng-controller="FileTreeController").full-size
|
|||
ng-controller="FileTreeRootFolderController",
|
||||
ng-class="{ 'no-toolbar': !permissions.write }"
|
||||
)
|
||||
|
||||
div(ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')")
|
||||
ul.list-unstyled.file-tree-list
|
||||
li(
|
||||
ng-class="{ 'selected': ui.view == 'pdf' }"
|
||||
ng-controller="PdfViewToggleController"
|
||||
)
|
||||
.entity
|
||||
.entity-name(
|
||||
ng-click="togglePdfView()"
|
||||
)
|
||||
i.fa.fa-fw.toggle
|
||||
i.fa.fa-fw.fa-file-pdf-o
|
||||
| PDF
|
||||
|
||||
ul.list-unstyled.file-tree-list(
|
||||
droppable="permissions.write"
|
||||
accept=".entity-name"
|
||||
on-drop-callback="onDrop"
|
||||
)
|
||||
li(
|
||||
ng-show="ui.pdfLayout == 'flat' && (ui.view == 'editor' || ui.view == 'pdf' || ui.view == 'file')"
|
||||
ng-class="{ 'selected': ui.view == 'pdf' }"
|
||||
ng-controller="PdfViewToggleController"
|
||||
)
|
||||
.entity
|
||||
.entity-name(
|
||||
ng-click="togglePdfView()"
|
||||
)
|
||||
i.fa.fa-fw.toggle
|
||||
i.fa.fa-fw.fa-file-pdf-o
|
||||
| PDF
|
||||
|
||||
file-entity(
|
||||
entity="entity",
|
||||
permissions="permissions",
|
||||
|
@ -81,27 +80,25 @@ aside#file-tree(ng-controller="FileTreeController").full-size
|
|||
)
|
||||
.entity
|
||||
.entity-name(
|
||||
ng-click="select()"
|
||||
ng-click="select($event)"
|
||||
)
|
||||
//- Just a spacer to align with folders
|
||||
i.fa.fa-fw.toggle
|
||||
i.fa.fa-fw.fa-file
|
||||
|
||||
span {{ entity.name }}
|
||||
|
||||
|
||||
|
||||
|
||||
script(type='text/ng-template', id='entityListItemTemplate')
|
||||
li(
|
||||
ng-class="{ 'selected': entity.selected }",
|
||||
ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }",
|
||||
ng-controller="FileTreeEntityController"
|
||||
)
|
||||
.entity(ng-if="entity.type != 'folder'")
|
||||
.entity-name(
|
||||
ng-click="select()"
|
||||
ng-click="select($event)"
|
||||
ng-dblclick="permissions.write && startRenaming()"
|
||||
draggable="permissions.write"
|
||||
draggable-helper="draggableHelper"
|
||||
context-menu
|
||||
data-target="context-menu-{{ entity.id }}"
|
||||
context-menu-container="body"
|
||||
|
@ -112,7 +109,6 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
|
||||
i.fa.fa-fw.fa-file(ng-if="entity.type == 'doc'")
|
||||
i.fa.fa-fw.fa-image(ng-if="entity.type == 'file'")
|
||||
|
||||
span(
|
||||
ng-hide="entity.renaming"
|
||||
) {{ entity.name }}
|
||||
|
@ -126,12 +122,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
on-enter="finishRenaming()"
|
||||
)
|
||||
|
||||
span.dropdown(
|
||||
span.dropdown.entity-menu-toggle(
|
||||
dropdown,
|
||||
ng-show="entity.selected",
|
||||
ng-if="permissions.write"
|
||||
)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
|
||||
i.fa.fa-chevron-down
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
|
@ -140,12 +135,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
href
|
||||
ng-click="startRenaming()"
|
||||
right-click="startRenaming()"
|
||||
ng-show="!entity.multiSelected"
|
||||
) #{translate("rename")}
|
||||
li
|
||||
a(
|
||||
href
|
||||
ng-click="openDeleteModal()"
|
||||
right-click="openDeleteModal()"
|
||||
stop-propagation="click"
|
||||
) #{translate("delete")}
|
||||
|
||||
div.dropdown.context-menu(
|
||||
|
@ -158,20 +155,23 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
href
|
||||
ng-click="startRenaming()"
|
||||
right-click="startRenaming()"
|
||||
ng-show="!entity.multiSelected"
|
||||
) #{translate("rename")}
|
||||
li
|
||||
a(
|
||||
href
|
||||
ng-click="openDeleteModal()"
|
||||
right-click="openDeleteModal()"
|
||||
stop-propagation="click"
|
||||
) #{translate("delete")}
|
||||
|
||||
|
||||
.entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController")
|
||||
.entity-name(
|
||||
ng-click="select()"
|
||||
ng-click="select($event)"
|
||||
ng-dblclick="permissions.write && startRenaming()"
|
||||
draggable="permissions.write"
|
||||
draggable-helper="draggableHelper"
|
||||
droppable="permissions.write"
|
||||
accept=".entity-name"
|
||||
on-drop-callback="onDrop"
|
||||
|
@ -194,7 +194,7 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
'fa-folder': !expanded, \
|
||||
'fa-folder-open': expanded \
|
||||
}"
|
||||
ng-click="select()"
|
||||
ng-click="select($event)"
|
||||
)
|
||||
|
||||
span(
|
||||
|
@ -210,12 +210,11 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
on-enter="finishRenaming()"
|
||||
)
|
||||
|
||||
span.dropdown(
|
||||
span.dropdown.entity-menu-toggle(
|
||||
dropdown,
|
||||
ng-if="permissions.write"
|
||||
ng-show="entity.selected"
|
||||
)
|
||||
a.dropdown-toggle(href, dropdown-toggle)
|
||||
a.dropdown-toggle(href, dropdown-toggle, stop-propagation="click")
|
||||
i.fa.fa-chevron-down
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right
|
||||
|
@ -224,12 +223,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
href
|
||||
ng-click="startRenaming()"
|
||||
right-click="startRenaming()"
|
||||
ng-show="!entity.multiSelected"
|
||||
) #{translate("rename")}
|
||||
li
|
||||
a(
|
||||
href
|
||||
ng-click="openDeleteModal()"
|
||||
right-click="openDeleteModal()"
|
||||
stop-propagation="click"
|
||||
) #{translate("delete")}
|
||||
li.divider
|
||||
li
|
||||
|
@ -261,12 +262,14 @@ script(type='text/ng-template', id='entityListItemTemplate')
|
|||
href
|
||||
ng-click="startRenaming()"
|
||||
right-click="startRenaming()"
|
||||
ng-show="!entity.multiSelected"
|
||||
) #{translate("rename")}
|
||||
li
|
||||
a(
|
||||
href
|
||||
ng-click="openDeleteModal()"
|
||||
right-click="openDeleteModal()"
|
||||
stop-propagation="click"
|
||||
) #{translate("delete")}
|
||||
li.divider
|
||||
li
|
||||
|
@ -306,6 +309,7 @@ script(type='text/ng-template', id='newDocModalTemplate')
|
|||
h3 #{translate("new_file")}
|
||||
.modal-body
|
||||
form(novalidate, name="newDocForm")
|
||||
div.alert.alert-danger(ng-if="error") {{error}}
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="File Name",
|
||||
|
@ -330,6 +334,7 @@ script(type='text/ng-template', id='newFolderModalTemplate')
|
|||
.modal-header
|
||||
h3 #{translate("new_folder")}
|
||||
.modal-body
|
||||
div.alert.alert-danger(ng-if="error") {{error}}
|
||||
form(novalidate, name="newFolderForm")
|
||||
input.form-control(
|
||||
type="text",
|
||||
|
@ -354,9 +359,20 @@ script(type='text/ng-template', id='newFolderModalTemplate')
|
|||
script(type="text/ng-template", id="uploadFileModalTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("upload_files")}
|
||||
span
|
||||
.alert.alert-warning.small(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
|
||||
.alert.alert-warning.small(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
|
||||
.alert.alert-warning.small.modal-alert(ng-if="conflicts.length > 0")
|
||||
p.text-center
|
||||
| The following files already exist in this project:
|
||||
ul.text-center.list-unstyled.row-spaced-small
|
||||
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
|
||||
p.text-center.row-spaced-small
|
||||
| Do you want to overwrite them?
|
||||
p.text-center
|
||||
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
|
||||
|
|
||||
a(href, ng-click="cancel()").btn.btn-default Cancel
|
||||
|
||||
.modal-body(
|
||||
fine-upload
|
||||
|
@ -367,10 +383,14 @@ script(type="text/ng-template", id="uploadFileModalTemplate")
|
|||
drag-area-text="{{drag_files}}"
|
||||
hint-text="{{hint_press_and_hold_control_key}}"
|
||||
multiple="true"
|
||||
auto-upload="false"
|
||||
on-complete-callback="onComplete"
|
||||
on-upload-callback="onUpload"
|
||||
on-validate-batch="onValidateBatch"
|
||||
on-error-callback="onError"
|
||||
on-submit-callback="onSubmit"
|
||||
on-cancel-callback="onCancel"
|
||||
control="control"
|
||||
params="{'folder_id': parent_folder_id}"
|
||||
)
|
||||
span #{translate("upload_files")}
|
||||
|
@ -382,6 +402,8 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
|
|||
h3 #{translate("delete")} {{ entity.name }}
|
||||
.modal-body
|
||||
p !{translate("sure_you_want_to_delete")}
|
||||
ul
|
||||
li(ng-repeat="entity in entities") {{entity.name}}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
|
|
|
@ -50,16 +50,22 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
|
|||
.hotkey
|
||||
span.combination {{ctrl}} + A
|
||||
span.description Select All
|
||||
.col-xs-6
|
||||
.hotkey
|
||||
span.combination Tab
|
||||
span.description Indent Selection
|
||||
.col-xs-6
|
||||
.hotkey
|
||||
span.combination {{ctrl}} + U
|
||||
span.combination Ctrl + U
|
||||
span.description To Uppercase
|
||||
.hotkey
|
||||
span.combination Ctrl + Shift + U
|
||||
span.description To Lowercase
|
||||
.hotkey
|
||||
span.combination {{ctrl}} + B
|
||||
span.description Bold text
|
||||
.hotkey
|
||||
span.combination {{ctrl}} + I
|
||||
span.description Italic Text
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()"
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
div.full-size.pdf(ng-controller="PdfController")
|
||||
.toolbar.toolbar-tall
|
||||
a.btn.btn-info(
|
||||
href,
|
||||
ng-disabled="pdf.compiling",
|
||||
ng-click="recompile()"
|
||||
)
|
||||
i.fa.fa-refresh(
|
||||
ng-class="{'fa-spin': pdf.compiling }"
|
||||
.btn-group(dropdown)
|
||||
a.btn.btn-info(
|
||||
href,
|
||||
ng-disabled="pdf.compiling",
|
||||
ng-click="recompile()"
|
||||
)
|
||||
|
|
||||
span(ng-show="!pdf.compiling") #{translate("recompile")}
|
||||
span(ng-show="pdf.compiling") #{translate("compiling")}...
|
||||
i.fa.fa-refresh(
|
||||
ng-class="{'fa-spin': pdf.compiling }"
|
||||
)
|
||||
|
|
||||
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}")
|
||||
| #{translate("normal")}
|
||||
li
|
||||
a(href, ng-click="draft = true")
|
||||
i.fa.fa-fw(ng-class="{'fa-check': draft}")
|
||||
| #{translate("fast")}
|
||||
span.subdued [draft]
|
||||
a.log-btn(
|
||||
href
|
||||
ng-click="toggleLogs()"
|
||||
|
@ -94,41 +112,43 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
| #{translate("learn_how_to_make_documents_compile_quickly")}
|
||||
|
||||
.alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile")
|
||||
p
|
||||
strong #{translate("upgrade_for_faster_compiles")}
|
||||
h5 #{translate("free_accounts_have_timeout_upgrade_to_increase")}
|
||||
h4 Plus:
|
||||
h4
|
||||
p
|
||||
strong #{translate("upgrade_for_faster_compiles")}
|
||||
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
|
||||
p Plus:
|
||||
p
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
|
||||
p(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
sixpack-convert="track_changes_feature_info"
|
||||
ng-click="startFreeTrial('compile-timeout')"
|
||||
) #{translate("start_free_trial")}
|
||||
p(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success.row-spaced-small(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
sixpack-convert="track_changes_feature_info"
|
||||
ng-click="startFreeTrial('compile-timeout')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
.pdf-errors(ng-show="pdf.projectTooLarge")
|
||||
.alert.alert-danger
|
||||
|
|
|
@ -6,46 +6,84 @@ script(type="text/ng-template", id="publishProjectAsTemplateModalTemplate")
|
|||
ng-click="cancel()"
|
||||
) ×
|
||||
h3 #{translate("publish_as_template")}
|
||||
.modal-body.modal-body-share
|
||||
span(ng-hide="problemTalkingToTemplateApi")
|
||||
form()
|
||||
label(for='Description') #{translate("template_description")}
|
||||
.form-group
|
||||
textarea.form-control(
|
||||
rows=5,
|
||||
name='Description',
|
||||
ng-model="templateDetails.description",
|
||||
value=""
|
||||
)
|
||||
div(ng-show="templateDetails.exists").text-center.templateDetails
|
||||
| #{translate("project_last_published_at")}
|
||||
strong {{templateDetails.publishedDate}}.
|
||||
a(ng-href="{{templateDetails.canonicalUrl}}") #{translate("view_in_template_gallery")}.
|
||||
div(ng-if="project.features.templates")
|
||||
.modal-body.modal-body-share
|
||||
span(ng-hide="problemTalkingToTemplateApi")
|
||||
form()
|
||||
label(for='Description') #{translate("template_description")}
|
||||
.form-group
|
||||
textarea.form-control(
|
||||
rows=5,
|
||||
name='Description',
|
||||
ng-model="templateDetails.description",
|
||||
value=""
|
||||
)
|
||||
div(ng-show="templateDetails.exists").text-center.templateDetails
|
||||
| #{translate("project_last_published_at")}
|
||||
strong {{templateDetails.publishedDate}}.
|
||||
a(ng-href="{{templateDetails.canonicalUrl}}") #{translate("view_in_template_gallery")}.
|
||||
|
||||
span(ng-show="problemTalkingToTemplateApi") #{translate("problem_talking_to_publishing_service")}.
|
||||
span(ng-show="problemTalkingToTemplateApi") #{translate("problem_talking_to_publishing_service")}.
|
||||
|
||||
.modal-footer(ng-hide="problemTalkingToTemplateApi")
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()",
|
||||
ng-disabled="state.publishInflight || state.unpublishInflight"
|
||||
)
|
||||
span #{translate("cancel")}
|
||||
|
||||
button.btn.btn-info(
|
||||
ng-click="unpublishTemplate()",
|
||||
ng-disabled="state.publishInflight || state.unpublishInflight"
|
||||
ng-show="templateDetails.exists"
|
||||
)
|
||||
span(ng-show="!state.unpublishInflight") #{translate("unpublish")}
|
||||
span(ng-show="state.unpublishInflight") #{translate("unpublishing")}...
|
||||
|
||||
button.btn.btn-primary(
|
||||
ng-click="publishTemplate()",
|
||||
ng-disabled="state.publishInflight || state.unpublishInflight"
|
||||
)
|
||||
span(ng-show="!state.publishInflight && !templateDetails.exists") #{translate("publish")}
|
||||
span(ng-show="!state.publishInflight && templateDetails.exists") #{translate("republish")}
|
||||
span(ng-show="state.publishInflight") #{translate("publishing")}...
|
||||
|
||||
div(ng-hide="project.features.templates")
|
||||
.modal-body.modal-body-share
|
||||
p #{translate("upgrade_to_get_feature", {feature:"templates"})}
|
||||
|
||||
.modal-footer(ng-hide="problemTalkingToTemplateApi")
|
||||
button.btn.btn-default(
|
||||
ng-click="cancel()",
|
||||
ng-disabled="state.publishInflight || state.unpublishInflight"
|
||||
)
|
||||
span #{translate("cancel")}
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
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")}...
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
button.btn.btn-primary(
|
||||
ng-click="publishTemplate()",
|
||||
ng-disabled="state.publishInflight || state.unpublishInflight"
|
||||
)
|
||||
span(ng-show="!state.publishInflight && !templateDetails.exists") #{translate("publish")}
|
||||
span(ng-show="!state.publishInflight && templateDetails.exists") #{translate("republish")}
|
||||
span(ng-show="state.publishInflight") #{translate("publishing")}...
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
|
||||
.modal-footer(ng-controller="FreeTrialModalController")
|
||||
.text-center
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('templates')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
.row-spaced-small.small(ng-show="startedFreeTrial")
|
||||
| #{translate("refresh_page_after_starting_free_trial")}
|
||||
|
|
|
@ -79,33 +79,36 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
type="submit"
|
||||
ng-mousedown="addMembers()"
|
||||
) #{translate("share")}
|
||||
div.text-center(ng-hide="canAddCollaborators")
|
||||
p #{translate("need_to_upgrade_for_more_collabs")}.
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
div(ng-hide="canAddCollaborators")
|
||||
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
|
||||
p(ng-controller="FreeTrialModalController")
|
||||
p.text-center.row-spaced-thin(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
|
@ -122,7 +125,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
|
||||
button.btn.btn-primary(
|
||||
ng-click="done()"
|
||||
) #{translate("done")}
|
||||
) #{translate("close")}
|
||||
|
||||
script(type="text/ng-template", id="makePublicModalTemplate")
|
||||
.modal-header
|
||||
|
|
|
@ -1,83 +1,39 @@
|
|||
div#trackChanges(ng-show="ui.view == 'track-changes'")
|
||||
span(ng-controller="TrackChangesPremiumPopup")
|
||||
span(ng-if="versioningPopupType == 'default'")
|
||||
.upgrade-prompt(ng-show="!project.features.versioning")
|
||||
.message(ng-show="project.owner._id == user.id")
|
||||
|
||||
p #{translate("upgrade_to_get_feature", {feature:"Entire Doc History"})}
|
||||
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
h4
|
||||
.upgrade-prompt(ng-show="!project.features.versioning")
|
||||
.message(ng-show="project.owner._id == user.id")
|
||||
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
|
||||
p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
ul.list-unstyled
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
h4
|
||||
li
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
|
||||
p(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
|
||||
|
||||
.message(ng-show="project.owner._id != user.id")
|
||||
p #{translate("ask_proj_owner_to_upgrade_for_history")}
|
||||
p
|
||||
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
|
||||
|
||||
.upgrade-prompt(ng-show="!project.features.versioning", ng-if="versioningPopupType == 'discount'")
|
||||
.message(ng-show="project.owner._id == user.id")
|
||||
|
||||
p #{translate("upgrade_to_get_feature", {feature:"Entire Doc History"})}
|
||||
h2(style="color:#a93529;") 20% Off First 6 Months!
|
||||
p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("unlimited_projects")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("collabs_per_proj", {collabcount:'Multiple'})}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("full_doc_history")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_dropbox")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
| #{translate("sync_to_github")}
|
||||
|
||||
h4
|
||||
i.fa.fa-check
|
||||
|#{translate("compile_larger_projects")}
|
||||
p(ng-controller="FreeTrialModalController")
|
||||
p.text-center(ng-controller="FreeTrialModalController")
|
||||
a.btn.btn-success(
|
||||
href
|
||||
ng-class="buttonClass"
|
||||
ng-click="startFreeTrial('track-changes', 'cf3yutfzu7ztxz')"
|
||||
ng-click="startFreeTrial('track-changes')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ block content
|
|||
script(type="text/javascript").
|
||||
window.data = {
|
||||
projects: !{JSON.stringify(projects).replace(/\//g, '\\/')},
|
||||
tags: !{JSON.stringify(tags).replace(/\//g, '\\/')}
|
||||
tags: !{JSON.stringify(tags).replace(/\//g, '\\/')},
|
||||
notifications: !{JSON.stringify(notifications).replace(/\//g, '\\/')}
|
||||
};
|
||||
window.algolia = {
|
||||
institutions: {
|
||||
|
@ -20,12 +21,14 @@ block content
|
|||
|
||||
.content.content-alt(ng-controller="ProjectPageController")
|
||||
.container
|
||||
|
||||
.row(ng-cloak)
|
||||
span(ng-show="first_sign_up == 'default' || projects.length > 0")
|
||||
aside.col-md-2.col-xs-3
|
||||
include ./list/side-bar
|
||||
|
||||
.col-md-10.col-xs-9
|
||||
include ./list/notifications
|
||||
include ./list/project-list
|
||||
|
||||
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
|
||||
|
|
|
@ -18,6 +18,8 @@ script(type='text/ng-template', id='newTagModalTemplate')
|
|||
stop-propagation="click"
|
||||
)
|
||||
.modal-footer
|
||||
.modal-footer-left
|
||||
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
|
||||
//- We stop propagation to stop the clicks from closing the
|
||||
//- 'move to folder' menu.
|
||||
button.btn.btn-default(
|
||||
|
@ -25,10 +27,67 @@ script(type='text/ng-template', id='newTagModalTemplate')
|
|||
stop-propagation="click"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="newTagForm.$invalid"
|
||||
ng-disabled="newTagForm.$invalid || state.inflight"
|
||||
ng-click="create()"
|
||||
stop-propagation="click"
|
||||
) #{translate("create")}
|
||||
)
|
||||
span(ng-show="!state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
script(type='text/ng-template', id='deleteTagModalTemplate')
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="cancel()"
|
||||
) ×
|
||||
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()"
|
||||
) ×
|
||||
h3 #{translate("rename_folder")}
|
||||
.modal-body
|
||||
form(name="renameTagForm", novalidate)
|
||||
input.form-control(
|
||||
type="text",
|
||||
placeholder="Tag Name",
|
||||
ng-model="inputs.tagName",
|
||||
required,
|
||||
on-enter="rename()",
|
||||
focus-on="open"
|
||||
)
|
||||
.modal-footer
|
||||
.modal-footer-left
|
||||
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
|
||||
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-click="rename()",
|
||||
ng-disabled="renameTagForm.$invalid || state.inflight"
|
||||
)
|
||||
span(ng-show="!state.inflight") #{translate("rename")}
|
||||
span(ng-show="state.inflight") #{translate("renaming")}...
|
||||
|
||||
script(type='text/ng-template', id='renameProjectModalTemplate')
|
||||
.modal-header
|
||||
|
|
15
services/web/app/views/project/list/notifications.jade
Normal file
15
services/web/app/views/project/list/notifications.jade
Normal 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") ×
|
||||
span.sr-only #{translate("close")}
|
|
@ -58,7 +58,7 @@
|
|||
)
|
||||
li.dropdown-header #{translate("add_to_folder")}
|
||||
li(
|
||||
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
|
||||
ng-repeat="tag in tags | orderBy:'name'",
|
||||
ng-controller="TagDropdownItemController"
|
||||
)
|
||||
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
|
||||
|
@ -79,7 +79,7 @@
|
|||
href='#',
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
) #{translate("more")}
|
||||
) #{translate("more")}
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right(role="menu")
|
||||
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
|
||||
|
|
|
@ -37,22 +37,23 @@
|
|||
ul.list-unstyled.folders-menu(
|
||||
ng-controller="TagListController"
|
||||
)
|
||||
li(ng-class="{active: (filter == 'all')}")
|
||||
a(href, ng-click="filterProjects('all')") #{translate("all_projects")}
|
||||
li(ng-class="{active: (filter == 'owned')}")
|
||||
a(href, ng-click="filterProjects('owned')") #{translate("your_projects")}
|
||||
li(ng-class="{active: (filter == 'shared')}")
|
||||
a(href, ng-click="filterProjects('shared')") #{translate("shared_with_you")}
|
||||
li(ng-class="{active: (filter == 'archived')}")
|
||||
a(href, ng-click="filterProjects('archived')") #{translate("deleted_projects")}
|
||||
li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')")
|
||||
a(href) #{translate("all_projects")}
|
||||
li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')")
|
||||
a(href) #{translate("your_projects")}
|
||||
li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')")
|
||||
a(href) #{translate("shared_with_you")}
|
||||
li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')")
|
||||
a(href) #{translate("deleted_projects")}
|
||||
li
|
||||
h2 #{translate("folders")}
|
||||
li(
|
||||
ng-repeat="tag in tags | filter:nonEmpty",
|
||||
li.tag(
|
||||
ng-repeat="tag in tags | orderBy:name",
|
||||
ng-class="{active: tag.selected}",
|
||||
ng-cloak
|
||||
ng-cloak,
|
||||
ng-click="selectTag(tag)"
|
||||
)
|
||||
a.tag(href, ng-click="selectTag(tag)")
|
||||
a.tag-name(href)
|
||||
i.icon.fa.fa-fw(
|
||||
ng-class="{\
|
||||
'fa-folder-open-o': tag.selected,\
|
||||
|
@ -61,6 +62,23 @@
|
|||
)
|
||||
span.name {{tag.name}}
|
||||
span.subdued ({{tag.project_ids.length}})
|
||||
span.dropdown.tag-menu(dropdown)
|
||||
a.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle,
|
||||
stop-propagation="click"
|
||||
)
|
||||
span.caret
|
||||
ul.dropdown-menu.dropdown-menu-right(
|
||||
role="menu"
|
||||
)
|
||||
li
|
||||
a(href, ng-click="renameTag(tag)", stop-propagation="click")
|
||||
| #{translate("rename")}
|
||||
li
|
||||
a(href, ng-click="deleteTag(tag)", stop-propagation="click")
|
||||
| #{translate("delete")}
|
||||
li(ng-cloak)
|
||||
a.tag(href, ng-click="openNewTagModal()")
|
||||
i.fa.fa-fw.fa-plus
|
||||
|
@ -102,41 +120,51 @@
|
|||
) #{translate("complete")}
|
||||
|
||||
|
||||
.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgrade-reason").text-centered
|
||||
.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
|
||||
span(sixpack-default).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(sixpack-when="dropbox").text-centered
|
||||
hr
|
||||
.card.card-thin
|
||||
span(sixpack-when="random").text-centered
|
||||
span(ng-if="randomView == 'default'")
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
span Get Dropbox Sync
|
||||
p
|
||||
img(src="/img/dropbox/simple_logo.png")
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(sixpack-when="github").text-centered
|
||||
hr
|
||||
.card.card-thin
|
||||
p
|
||||
span Get Github Sync
|
||||
p
|
||||
img(src="/img/github/octocat.jpg")
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgrade-reason").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
span(ng-if="randomView == 'dropbox'")
|
||||
hr
|
||||
.card.card-thin
|
||||
p
|
||||
span Get Dropbox Sync
|
||||
p
|
||||
img(src="/img/dropbox/simple_logo.png")
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
|
||||
span(ng-if="randomView == 'github'")
|
||||
hr
|
||||
.card.card-thin
|
||||
p
|
||||
span Get Github Sync
|
||||
p
|
||||
img(src="/img/github/octocat.jpg")
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
script.
|
||||
window.userHasSubscription = #{settings.enableSubscriptions && !hasSubscription}
|
||||
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
'EBCallBackMessageReceived',
|
||||
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
|
||||
'conduitPage',
|
||||
'NS_ERROR_NOT_CONNECTED:',
|
||||
"TypeError: Cannot read property 'row' of undefined",
|
||||
'/NS_ERROR_NOT_CONNECTED/i',
|
||||
"/Cannot read property 'row' of undefined/i",
|
||||
'TypeError: start is undefined'
|
||||
],
|
||||
ignoreUrls: [
|
||||
|
@ -57,7 +57,7 @@
|
|||
],
|
||||
shouldSendCallback: function(data) {
|
||||
// only send a fraction of errors
|
||||
var sampleRate = 1.00;
|
||||
var sampleRate = 0.01;
|
||||
return (Math.random() <= sampleRate);
|
||||
},
|
||||
dataCallback: function(data) {
|
||||
|
|
|
@ -8,7 +8,7 @@ block content
|
|||
.card
|
||||
.page-header
|
||||
h1 #{translate("your_subscription")}
|
||||
div To make changes to your subscription please contact team@sharelatex.com
|
||||
div To make changes to your subscription please contact accounts@sharelatex.com
|
||||
div
|
||||
div
|
||||
-if(subscription.groupPlan)
|
||||
|
|
|
@ -6,7 +6,7 @@ block scripts
|
|||
window.recomendedCurrency = '#{recomendedCurrency}'
|
||||
window.recurlyApiKey = "!{settings.apis.recurly.publicKey}"
|
||||
window.taxRate = #{taxRate}
|
||||
|
||||
window.subscription = !{JSON.stringify(subscription)}
|
||||
|
||||
|
||||
|
||||
|
@ -36,32 +36,32 @@ mixin printPlans(plans)
|
|||
mixin printPlan(plan)
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container
|
||||
.content.content-alt(ng-cloak)
|
||||
.container(ng-controller="UserSubscriptionController")
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.card
|
||||
.card(ng-if="view == 'overview'")
|
||||
.page-header
|
||||
h1 #{translate("your_subscription")}
|
||||
-if (groups.length != 0)
|
||||
each groupSubscription in groups
|
||||
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
||||
span
|
||||
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
|
||||
- else if (subscription)
|
||||
case subscription.state
|
||||
when "free-trial"
|
||||
p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})}
|
||||
p !{translate("choose_a_plan_below")}
|
||||
|
||||
when "active"
|
||||
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
|
||||
a(href, ng-click="changePlan = true") !{translate("change_plan")}.
|
||||
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>" + subscription.price + "</strong>", collectionDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
|
||||
p.pull-right
|
||||
|
||||
p: form(action="/user/subscription/cancel",method="post")
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
p
|
||||
a(href="/user/subscription/billing-details/edit").btn.btn-info #{translate("update_your_billing_details")}
|
||||
|
|
||||
input(type="submit", value="Cancel your subscription").btn.btn-primary#cancelSubscription
|
||||
a(href, ng-click="switchToCancelationView()").btn.btn-primary !{translate("cancel_your_subscription")}
|
||||
when "canceled"
|
||||
p !{translate("currently_subscribed_to_plan", {planName:"<strong>" + subscription.name + "</strong>"})}
|
||||
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"<strong>" + subscription.nextPaymentDueAt + "</strong>"})}
|
||||
|
@ -76,7 +76,7 @@ block content
|
|||
p !{translate("problem_with_subscription_contact_us")}
|
||||
|
||||
-if(subscription.groupPlan)
|
||||
a(href="/subscription/group").btn.btn-success !{translate("manage_group")}
|
||||
a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
|
||||
|
||||
|
||||
|
||||
|
@ -105,17 +105,50 @@ block content
|
|||
mixin printPlans(plans.individualMonthlyPlans)
|
||||
mixin printPlans(plans.individualAnnualPlans)
|
||||
|
||||
.card(ng-if="view == 'cancelation'")
|
||||
.page-header
|
||||
h1 #{translate("Cancel Subscription")}
|
||||
|
||||
span(ng-if="sixpackOpt == 'downgrade-options'")
|
||||
|
||||
div(ng-show="showExtendFreeTrial", style="text-align: center")
|
||||
p !{translate("have_more_days_to_try", {days:14})}
|
||||
button(type="submit", ng-click="exendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")}
|
||||
p
|
||||
|
|
||||
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
|
||||
|
|
||||
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")}
|
||||
|
|
||||
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")}
|
||||
|
|
||||
a(ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
|
||||
|
||||
script(type="text/javascript").
|
||||
$('#cancelSubscription').on("click", function() {
|
||||
ga('send', 'event', 'subscription-funnel', 'cancelation')
|
||||
})
|
||||
|
||||
|
||||
script(type='text/ng-template', id='confirmChangePlanModalTemplate')
|
||||
.modal-header
|
||||
h3 Change plan?
|
||||
h3 #{translate("change_plan")}
|
||||
.modal-body
|
||||
p !{translate("sure_you_want_to_change_plan", {planName:"<strong>{{plan.name}}</strong>"})}
|
||||
.modal-footer
|
||||
|
@ -131,4 +164,22 @@ block content
|
|||
span(ng-show="inflight") #{translate("processing")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='LeaveGroupModalTemplate')
|
||||
.modal-header
|
||||
h3 #{translate("leave_group")}
|
||||
.modal-body
|
||||
p #{translate("sure_you_want_to_leave_group")}
|
||||
.modal-footer
|
||||
button.btn.btn-default(
|
||||
ng-disabled="inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-danger(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="confirmLeaveGroup()"
|
||||
)
|
||||
span(ng-hide="inflight") #{translate("leave_now")}
|
||||
span(ng-show="inflight") #{translate("processing")}...
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -112,7 +112,6 @@ block content
|
|||
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
|
||||
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
|
||||
option(value="", disabled, selected) Year
|
||||
option(value="2015") 2015
|
||||
option(value="2016") 2016
|
||||
option(value="2017") 2017
|
||||
option(value="2018") 2018
|
||||
|
|
|
@ -3,6 +3,12 @@ block scripts
|
|||
script(type='text/javascript').
|
||||
window.recomendedCurrency = '#{recomendedCurrency}'
|
||||
window.abCurrencyFlag = '#{abCurrencyFlag}'
|
||||
|
||||
script(type='text/javascript').
|
||||
(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true;
|
||||
s.src=('https:'==document.location.protocol?'https':'http') + '://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket/api.js';
|
||||
var q = document.getElementsByTagName('script')[0];q.parentNode.insertBefore(s, q);})();
|
||||
|
||||
block content
|
||||
.content-alt
|
||||
.content.plans(ng-controller="PlansController")
|
||||
|
@ -186,30 +192,30 @@ block content
|
|||
.modal-header
|
||||
h3 #{translate("group_plan_enquiry")}
|
||||
.modal-body
|
||||
form(name='form1', autocomplete='off', enctype='multipart/form-data', method='post', novalidate='', action='https://sharelatex.wufoo.com/forms/z7x3p3/#public', _lpchecked='1')
|
||||
.form-group
|
||||
label(for='Field9') #{translate("name")}
|
||||
input.form-control(name='Field9', type='text', value='', maxlength='255', tabindex='1', onkeyup='')
|
||||
|
||||
.form-group
|
||||
label(for='Field11') #{translate("email")}
|
||||
input.form-control(name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
|
||||
.form-group
|
||||
label(for='Field12') #{translate("university")}
|
||||
input.form-control(name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
|
||||
|
||||
.form-group
|
||||
label(for='Field13') #{translate("position")}
|
||||
input.form-control(name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
|
||||
.form-group
|
||||
input.btn.btn-primary.btn-large(name='saveForm', type='submit', value='Send')
|
||||
div(style='display: none;')
|
||||
label(for='comment') Do Not Fill This Out
|
||||
textarea#comment(name='comment', rows='1', cols='1')
|
||||
input#idstamp(type='hidden', name='idstamp', value='xkgLkZnS/AQW71jCS1d0XrrFjq26lJryIPVk2rx0YkU=')
|
||||
|
||||
form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak)
|
||||
span(ng-show="sent == false")
|
||||
.form-group
|
||||
label#title9(for='Field9')
|
||||
| Name
|
||||
input#Field9.field.text.medium.span8.form-control(ng-model="form.name", maxlength='255', tabindex='1', onkeyup='')
|
||||
label#title11.desc(for='Field11')
|
||||
| Email
|
||||
.form-group
|
||||
input#Field11.field.text.medium.span8.form-control(ng-model="form.email", name='Field11', type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
label#title12.desc(for='Field12')
|
||||
| University / Company
|
||||
.form-group
|
||||
input#Field12.field.text.medium.span8.form-control(ng-model="form.university", name='Field12', type='text', value='', maxlength='255', tabindex='3', onkeyup='')
|
||||
label#title13.desc(for='Field13')
|
||||
| Position
|
||||
.form-group
|
||||
input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
.form-group
|
||||
input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';")
|
||||
.form-group.text-center
|
||||
input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote')
|
||||
span(ng-show="sent")
|
||||
p Request Sent, Thank you.
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
|
|
|
@ -5,14 +5,18 @@ block content
|
|||
.container
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
.card
|
||||
.card(ng-cloak)
|
||||
.page-header
|
||||
h2 #{translate("thanks_for_subscribing")}
|
||||
.alert.alert-success
|
||||
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"<strong>"+subscription.price+"</strong>", collectionDate:"<strong>"+subscription.nextPaymentDueAt+"</strong>"})}
|
||||
p #{translate("if_you_dont_want_to_be_charged")}
|
||||
a(href="/user/subscription") #{translate("click_here_to_cancel")}.
|
||||
|
||||
span(sixpack-switch="upgrade-success-message")
|
||||
span(sixpack-default)
|
||||
p #{translate("if_you_dont_want_to_be_charged")}
|
||||
a(href="/user/subscription") #{translate("click_here_to_cancel")}.
|
||||
span(sixpack-when="manage-subscription")
|
||||
p #{translate("to_modify_your_subscription_go_to")}
|
||||
a(href="/user/subscription") #{translate("manage_subscription")}.
|
||||
p
|
||||
- if (subscription.groupPlan == true)
|
||||
a.btn.btn-success.btn-large(href="/subscription/group") #{translate("add_your_first_group_member_now")}
|
||||
|
|
64
services/web/app/views/user/activate.jade
Normal file
64
services/web/app/views/user/activate.jade
Normal 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 || {})}
|
|
@ -20,6 +20,7 @@ block content
|
|||
placeholder='email@example.com',
|
||||
ng-model="email",
|
||||
ng-model-options="{ updateOn: 'blur' }",
|
||||
ng-init="email = #{JSON.stringify(email)}",
|
||||
focus="true"
|
||||
)
|
||||
span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
|
||||
|
|
|
@ -16,10 +16,11 @@ block content
|
|||
ng-cloak
|
||||
)
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
form-messages(for="passwordResetForm")
|
||||
.alert.alert-success(ng-show="passwordResetForm.response.success")
|
||||
| #{translate("password_has_been_reset")}.
|
||||
a(href='/login') #{translate("login_here")}
|
||||
.alert.alert-success(ng-show="passwordResetForm.response.success")
|
||||
| #{translate("password_has_been_reset")}.
|
||||
a(href='/login') #{translate("login_here")}
|
||||
.alert.alert-danger(ng-show="passwordResetForm.response.error")
|
||||
| #{translate("password_reset_token_expired")}
|
||||
|
||||
.form-group
|
||||
input.form-control#passwordField(
|
||||
|
|
|
@ -106,6 +106,10 @@ module.exports =
|
|||
url: "http://localhost:3036"
|
||||
sixpack:
|
||||
url: ""
|
||||
references:
|
||||
url: "http://localhost:3040"
|
||||
notifications:
|
||||
url: "http://localhost:3042"
|
||||
|
||||
templates:
|
||||
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
|
||||
|
@ -125,6 +129,9 @@ module.exports =
|
|||
# Same, but with http auth credentials.
|
||||
httpAuthSiteUrl: 'http://#{httpAuthUser}:#{httpAuthPass}@localhost:3000'
|
||||
|
||||
|
||||
maxEntitiesPerProject: 2000
|
||||
|
||||
# Security
|
||||
# --------
|
||||
security:
|
||||
|
@ -143,6 +150,8 @@ module.exports =
|
|||
versioning: true
|
||||
compileTimeout: 60
|
||||
compileGroup: "standard"
|
||||
references: true
|
||||
templates: true
|
||||
|
||||
plans: plans = [{
|
||||
planCode: "personal"
|
||||
|
@ -291,7 +300,7 @@ module.exports =
|
|||
title: "ShareLaTeX Community Edition"
|
||||
|
||||
left_footer: [{
|
||||
text: "Powered by <a href='https://www.sharelatex.com'>ShareLaTeX</a> © 2015"
|
||||
text: "Powered by <a href='https://www.sharelatex.com'>ShareLaTeX</a> © 2016"
|
||||
}]
|
||||
|
||||
right_footer: [{
|
||||
|
|
|
@ -27,18 +27,23 @@
|
|||
"http-proxy": "^1.8.1",
|
||||
"jade": "~1.3.1",
|
||||
"ldapjs": "^0.7.1",
|
||||
<<<<<<< HEAD
|
||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
|
||||
=======
|
||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1",
|
||||
>>>>>>> master
|
||||
"lynx": "0.1.1",
|
||||
"marked": "^0.3.3",
|
||||
"method-override": "^2.3.3",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.2.0",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
|
||||
"mimelib": "0.2.14",
|
||||
"mocha": "1.17.1",
|
||||
"mongojs": "0.18.2",
|
||||
"mongoose": "4.1.0",
|
||||
"multer": "^0.1.8",
|
||||
"node-uuid": "1.4.1",
|
||||
"nodemailer": "0.6.1",
|
||||
"nodemailer": "2.1.0",
|
||||
"nodemailer-ses-transport": "^1.3.0",
|
||||
"optimist": "0.6.1",
|
||||
"redback": "0.4.0",
|
||||
"redis": "0.10.1",
|
||||
|
|
|
@ -16,7 +16,11 @@ define [
|
|||
onUploadCallback: "="
|
||||
onValidateBatch: "="
|
||||
onErrorCallback: "="
|
||||
onSubmitCallback: "="
|
||||
onCancelCallback: "="
|
||||
autoUpload: "="
|
||||
params: "="
|
||||
control: "="
|
||||
}
|
||||
link: (scope, element, attrs) ->
|
||||
multiple = scope.multiple or false
|
||||
|
@ -32,19 +36,27 @@ define [
|
|||
uploadButton: scope.uploadButtonText or "Upload"
|
||||
dragAreaText = scope.dragAreaText or "drag here"
|
||||
hintText = scope.hintText or ""
|
||||
|
||||
maxConnections = scope.maxConnections or 1
|
||||
onComplete = scope.onCompleteCallback or () ->
|
||||
onUpload = scope.onUploadCallback or () ->
|
||||
onError = scope.onErrorCallback or () ->
|
||||
onValidateBatch = scope.onValidateBatch or () ->
|
||||
onSubmit = scope.onSubmitCallback or () ->
|
||||
onCancel = scope.onCancelCallback or () ->
|
||||
if !scope.autoUpload?
|
||||
autoUpload = true
|
||||
else
|
||||
autoUpload = scope.autoUpload
|
||||
params = scope.params or {}
|
||||
params._csrf = window.csrfToken
|
||||
|
||||
q = new qq.FineUploader
|
||||
element: element[0]
|
||||
multiple: multiple
|
||||
autoUpload: autoUpload
|
||||
disabledCancelForFormUploads: true
|
||||
validation: validation
|
||||
maxConnections: maxConnections
|
||||
request:
|
||||
endpoint: endpoint
|
||||
forceMultipart: true
|
||||
|
@ -55,6 +67,8 @@ define [
|
|||
onUpload: onUpload
|
||||
onValidateBatch: onValidateBatch
|
||||
onError: onError
|
||||
onSubmit: onSubmit
|
||||
onCancel: onCancel
|
||||
text: text
|
||||
template: """
|
||||
<div class="qq-uploader">
|
||||
|
@ -69,5 +83,7 @@ define [
|
|||
<ul class="qq-upload-list"></ul>
|
||||
</div>
|
||||
"""
|
||||
window.q = q
|
||||
scope.control?.q = q
|
||||
return q
|
||||
}
|
|
@ -8,6 +8,7 @@ define [
|
|||
"ide/permissions/PermissionsManager"
|
||||
"ide/pdf/PdfManager"
|
||||
"ide/binary-files/BinaryFilesManager"
|
||||
"ide/references/ReferencesManager"
|
||||
"ide/settings/index"
|
||||
"ide/share/index"
|
||||
"ide/chat/index"
|
||||
|
@ -24,6 +25,7 @@ define [
|
|||
"directives/onEnter"
|
||||
"directives/stopPropagation"
|
||||
"directives/rightClick"
|
||||
"services/queued-http"
|
||||
"filters/formatDate"
|
||||
"main/event"
|
||||
"main/account-upgrade"
|
||||
|
@ -37,6 +39,7 @@ define [
|
|||
PermissionsManager
|
||||
PdfManager
|
||||
BinaryFilesManager
|
||||
ReferencesManager
|
||||
) ->
|
||||
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
|
||||
|
@ -66,12 +69,13 @@ define [
|
|||
|
||||
$scope.chat = {}
|
||||
|
||||
|
||||
|
||||
window._ide = ide
|
||||
|
||||
ide.project_id = $scope.project_id = window.project_id
|
||||
ide.$scope = $scope
|
||||
|
||||
ide.referencesSearchManager = new ReferencesManager(ide, $scope)
|
||||
ide.connectionManager = new ConnectionManager(ide, $scope)
|
||||
ide.fileTreeManager = new FileTreeManager(ide, $scope)
|
||||
ide.editorManager = new EditorManager(ide, $scope)
|
||||
|
@ -80,7 +84,7 @@ define [
|
|||
ide.pdfManager = new PdfManager(ide, $scope)
|
||||
ide.permissionsManager = new PermissionsManager(ide, $scope)
|
||||
ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
|
||||
|
||||
|
||||
inited = false
|
||||
$scope.$on "project:joined", () ->
|
||||
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.
|
||||
If the project has been renamed please look in your project list for a new project under the new name.
|
||||
""")
|
||||
|
||||
|
||||
DARK_THEMES = [
|
||||
"ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers",
|
||||
"merbivore", "merbivore_soft", "mono_industrial", "monokai",
|
||||
|
|
|
@ -37,8 +37,12 @@ define [
|
|||
|
||||
# Restore previously recorded state
|
||||
if (state = ide.localStorage("layout.#{name}"))?
|
||||
options.west = state.west
|
||||
options.east = state.east
|
||||
if state.east?
|
||||
if !attrs.minimumRestoreSizeEast? or (state.east.size >= attrs.minimumRestoreSizeEast and !state.east.initClosed)
|
||||
options.east = state.east
|
||||
if state.west?
|
||||
if !attrs.minimumRestoreSizeWest? or (state.west.size >= attrs.minimumRestoreSizeWest and !state.west.initClosed)
|
||||
options.west = state.west
|
||||
|
||||
repositionControls = () ->
|
||||
state = element.layout().readState()
|
||||
|
|
|
@ -36,6 +36,7 @@ define [
|
|||
@doc?.detachFromAce()
|
||||
editorDoc = @ace?.getSession().getDocument()
|
||||
editorDoc?.off "change", @_checkConsistency
|
||||
@ide.$scope.$emit 'document:closed', @doc
|
||||
|
||||
_checkConsistency: () ->
|
||||
# We've been seeing a lot of errors when I think there shouldn't be
|
||||
|
@ -116,6 +117,26 @@ define [
|
|||
flush: () ->
|
||||
@doc?.flushPendingOps()
|
||||
|
||||
chaosMonkey: (line = 0, char = "a") ->
|
||||
orig = char
|
||||
copy = null
|
||||
pos = 0
|
||||
timer = () =>
|
||||
unless copy? and copy.length
|
||||
copy = orig.slice() + ' ' + new Date() + '\n'
|
||||
line += if Math.random() > 0.1 then 1 else -2
|
||||
line = 0 if line < 0
|
||||
pos = 0
|
||||
char = copy[0]
|
||||
copy = copy.slice(1)
|
||||
@ace.session.insert({row: line, column: pos}, char)
|
||||
pos += 1
|
||||
@_cm = setTimeout timer, 100 + if Math.random() < 0.1 then 1000 else 0
|
||||
@_cm = timer()
|
||||
|
||||
clearChaosMonkey: () ->
|
||||
clearTimeout @_cm
|
||||
|
||||
pollSavedStatus: () ->
|
||||
# returns false if doc has ops waiting to be acknowledged or
|
||||
# sent that haven't changed since the last time we checked.
|
||||
|
|
|
@ -86,6 +86,7 @@ define [
|
|||
@_bindToDocumentEvents(doc, new_sharejs_doc)
|
||||
callback null, new_sharejs_doc
|
||||
|
||||
|
||||
_bindToDocumentEvents: (doc, sharejs_doc) ->
|
||||
sharejs_doc.on "error", (error, meta) =>
|
||||
if error?.message?.match "maxDocLength"
|
||||
|
@ -98,7 +99,7 @@ define [
|
|||
@ide.reportError(error, meta)
|
||||
@ide.showGenericMessageModal(
|
||||
"Out of sync"
|
||||
"Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
|
||||
"Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a href='http://sharelatex.tenderapp.com/help/kb/browsers/editor-out-of-sync-problems'>Please see this help guide for more information</a>"
|
||||
)
|
||||
@openDoc(doc, forceReopen: true)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ define [
|
|||
url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
|
||||
return url
|
||||
|
||||
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage) ->
|
||||
App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory) ->
|
||||
monkeyPatchSearch($rootScope, $compile)
|
||||
|
||||
return {
|
||||
|
@ -29,6 +29,7 @@ define [
|
|||
fontSize: "="
|
||||
autoComplete: "="
|
||||
sharejsDoc: "="
|
||||
spellCheck: "="
|
||||
spellCheckLanguage: "="
|
||||
highlights: "="
|
||||
text: "="
|
||||
|
@ -55,7 +56,9 @@ define [
|
|||
scope.name = attrs.aceEditor
|
||||
|
||||
autoCompleteManager = new AutoCompleteManager(scope, editor, element)
|
||||
spellCheckManager = new SpellCheckManager(scope, editor, element)
|
||||
if scope.spellCheck # only enable spellcheck when explicitly required
|
||||
spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
|
||||
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache)
|
||||
undoManager = new UndoManager(scope, editor, element)
|
||||
highlightsManager = new HighlightsManager(scope, editor, element)
|
||||
cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
|
||||
|
@ -69,6 +72,21 @@ define [
|
|||
editor.commands.removeCommand "transposeletters"
|
||||
editor.commands.removeCommand "showSettingsMenu"
|
||||
editor.commands.removeCommand "foldall"
|
||||
|
||||
# For European keyboards, the / is above 7 so needs Shift pressing.
|
||||
# This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment.
|
||||
# This doesn't do anything for LaTeX, so remap this to togglecomment to
|
||||
# work for European keyboards as normal.
|
||||
editor.commands.removeCommand "toggleBlockComment"
|
||||
editor.commands.removeCommand "togglecomment"
|
||||
|
||||
editor.commands.addCommand {
|
||||
name: "togglecomment",
|
||||
bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" },
|
||||
exec: (editor) -> editor.toggleCommentLines(),
|
||||
multiSelectAction: "forEachLine",
|
||||
scrollIntoView: "selectionPart"
|
||||
}
|
||||
|
||||
# Trigger search AND replace on CMD+F
|
||||
editor.commands.addCommand
|
||||
|
@ -77,7 +95,6 @@ define [
|
|||
exec: (editor) ->
|
||||
ace.require("ace/ext/searchbox").Search(editor, true)
|
||||
readOnly: true
|
||||
editor.commands.removeCommand "replace"
|
||||
|
||||
# Bold text on CMD+B
|
||||
editor.commands.addCommand
|
||||
|
|
|
@ -16,7 +16,7 @@ define [
|
|||
constructor: (@$scope, @editor) ->
|
||||
@suggestionManager = new SuggestionManager()
|
||||
|
||||
@monkeyPatchAutocomplete()
|
||||
@monkeyPatchAutocomplete()
|
||||
|
||||
@$scope.$watch "autoComplete", (autocomplete) =>
|
||||
if autocomplete
|
||||
|
@ -37,11 +37,47 @@ define [
|
|||
enableSnippets: true,
|
||||
enableLiveAutocompletion: false
|
||||
})
|
||||
|
||||
|
||||
SnippetCompleter =
|
||||
getCompletions: (editor, session, pos, prefix, callback) ->
|
||||
callback null, Snippets
|
||||
@editor.completers = [@suggestionManager, SnippetCompleter]
|
||||
|
||||
references = @$scope.$root._references
|
||||
ReferencesCompleter =
|
||||
getCompletions: (editor, session, pos, prefix, callback) ->
|
||||
range = new Range(pos.row, 0, pos.row, pos.column)
|
||||
lineUpToCursor = editor.getSession().getTextRange(range)
|
||||
commandFragment = getLastCommandFragment(lineUpToCursor)
|
||||
if commandFragment
|
||||
citeMatch = commandFragment.match(/^~?\\([a-z]*cite[a-z]?){(.*,)?(\w*)/)
|
||||
if citeMatch
|
||||
commandName = citeMatch[1]
|
||||
previousArgs = citeMatch[2]
|
||||
currentArg = citeMatch[3]
|
||||
if previousArgs == undefined
|
||||
previousArgs = ""
|
||||
previousArgsCaption = if previousArgs.length > 8 then "…," else previousArgs
|
||||
result = []
|
||||
result.push {
|
||||
caption: "\\#{commandName}{",
|
||||
snippet: "\\#{commandName}{",
|
||||
meta: "reference",
|
||||
score: 11000
|
||||
}
|
||||
if references.keys and references.keys.length > 0
|
||||
references.keys.forEach (key) ->
|
||||
if !(key in [null, undefined])
|
||||
result.push({
|
||||
caption: "\\#{commandName}{#{previousArgsCaption}#{key}",
|
||||
value: "\\#{commandName}{#{previousArgs}#{key}",
|
||||
meta: "reference",
|
||||
score: 10000
|
||||
})
|
||||
callback null, result
|
||||
else
|
||||
callback null, result
|
||||
|
||||
@editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter]
|
||||
|
||||
disable: () ->
|
||||
@editor.setOptions({
|
||||
|
@ -83,7 +119,7 @@ define [
|
|||
# since it will be adding in with the autocomplete of \begin{item}...
|
||||
if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
|
||||
editor.session.remove(range)
|
||||
|
||||
|
||||
Autocomplete::_insertMatch.call this, data
|
||||
|
||||
# Overwrite this to set autoInsert = false and set font size
|
||||
|
|
|
@ -14,7 +14,7 @@ define () ->
|
|||
caption: "\\begin{#{env}}..."
|
||||
snippet: """
|
||||
\\begin{#{env}}
|
||||
$1
|
||||
\t$1
|
||||
\\end{#{env}}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -24,8 +24,8 @@ define () ->
|
|||
caption: "\\begin{array}..."
|
||||
snippet: """
|
||||
\\begin{array}{${1:cc}}
|
||||
$2 & $3 \\\\\\\\
|
||||
$4 & $5
|
||||
\t$2 & $3 \\\\\\\\
|
||||
\t$4 & $5
|
||||
\\end{array}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -33,10 +33,10 @@ define () ->
|
|||
caption: "\\begin{figure}..."
|
||||
snippet: """
|
||||
\\begin{figure}
|
||||
\\centering
|
||||
\\includegraphics{$1}
|
||||
\\caption{${2:Caption}}
|
||||
\\label{${3:fig:my_label}}
|
||||
\t\\centering
|
||||
\t\\includegraphics{$1}
|
||||
\t\\caption{${2:Caption}}
|
||||
\t\\label{${3:fig:my_label}}
|
||||
\\end{figure}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -44,8 +44,8 @@ define () ->
|
|||
caption: "\\begin{tabular}..."
|
||||
snippet: """
|
||||
\\begin{tabular}{${1:c|c}}
|
||||
$2 & $3 \\\\\\\\
|
||||
$4 & $5
|
||||
\t$2 & $3 \\\\\\\\
|
||||
\t$4 & $5
|
||||
\\end{tabular}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -53,13 +53,13 @@ define () ->
|
|||
caption: "\\begin{table}..."
|
||||
snippet: """
|
||||
\\begin{table}[$1]
|
||||
\\centering
|
||||
\\begin{tabular}{${2:c|c}}
|
||||
$3 & $4 \\\\\\\\
|
||||
$5 & $6
|
||||
\\end{tabular}
|
||||
\\caption{${7:Caption}}
|
||||
\\label{${8:tab:my_label}}
|
||||
\t\\centering
|
||||
\t\\begin{tabular}{${2:c|c}}
|
||||
\t\t$3 & $4 \\\\\\\\
|
||||
\t\t$5 & $6
|
||||
\t\\end{tabular}
|
||||
\t\\caption{${7:Caption}}
|
||||
\t\\label{${8:tab:my_label}}
|
||||
\\end{table}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -67,7 +67,7 @@ define () ->
|
|||
caption: "\\begin{list}..."
|
||||
snippet: """
|
||||
\\begin{list}
|
||||
\\item $1
|
||||
\t\\item $1
|
||||
\\end{list}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -75,7 +75,7 @@ define () ->
|
|||
caption: "\\begin{enumerate}..."
|
||||
snippet: """
|
||||
\\begin{enumerate}
|
||||
\\item $1
|
||||
\t\\item $1
|
||||
\\end{enumerate}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -83,7 +83,7 @@ define () ->
|
|||
caption: "\\begin{itemize}..."
|
||||
snippet: """
|
||||
\\begin{itemize}
|
||||
\\item $1
|
||||
\t\\item $1
|
||||
\\end{itemize}
|
||||
"""
|
||||
meta: "env"
|
||||
|
@ -91,7 +91,7 @@ define () ->
|
|||
caption: "\\begin{frame}..."
|
||||
snippet: """
|
||||
\\begin{frame}{${1:Frame Title}}
|
||||
$2
|
||||
\t$2
|
||||
\\end{frame}
|
||||
"""
|
||||
meta: "env"
|
||||
|
|
|
@ -5,7 +5,7 @@ define [
|
|||
Range = ace.require("ace/range").Range
|
||||
|
||||
class SpellCheckManager
|
||||
constructor: (@$scope, @editor, @element) ->
|
||||
constructor: (@$scope, @editor, @element, @cache) ->
|
||||
$(document.body).append @element.find(".spell-check-menu")
|
||||
|
||||
@updatedLines = []
|
||||
|
@ -102,6 +102,8 @@ define [
|
|||
learnWord: (highlight) ->
|
||||
@apiRequest "/learn", word: highlight.word
|
||||
@highlightedWordManager.removeWord highlight.word
|
||||
language = @$scope.spellCheckLanguage
|
||||
@cache?.put("#{language}:#{highlight.word}", true)
|
||||
|
||||
getHighlightedWordAtCursor: () ->
|
||||
cursor = @editor.getCursorPosition()
|
||||
|
@ -143,24 +145,67 @@ define [
|
|||
runSpellCheck: (linesToProcess) ->
|
||||
{words, positions} = @getWords(linesToProcess)
|
||||
language = @$scope.spellCheckLanguage
|
||||
@apiRequest "/check", {language: language, words: words}, (error, result) =>
|
||||
if error? or !result? or !result.misspellings?
|
||||
return null
|
||||
|
||||
highlights = []
|
||||
seen = {}
|
||||
newWords = []
|
||||
newPositions = []
|
||||
|
||||
# iterate through all words, building up a list of
|
||||
# newWords/newPositions not in the cache
|
||||
for word, i in words
|
||||
key = "#{language}:#{word}"
|
||||
seen[key] ?= @cache.get(key) # avoid hitting the cache unnecessarily
|
||||
cached = seen[key]
|
||||
if not cached?
|
||||
newWords.push words[i]
|
||||
newPositions.push positions[i]
|
||||
else if cached is true
|
||||
# word is correct
|
||||
else
|
||||
highlights.push
|
||||
column: positions[i].column
|
||||
row: positions[i].row
|
||||
word: word
|
||||
suggestions: cached
|
||||
words = newWords
|
||||
positions = newPositions
|
||||
|
||||
displayResult = (highlights) =>
|
||||
if linesToProcess?
|
||||
for shouldProcess, row in linesToProcess
|
||||
@highlightedWordManager.clearRows(row, row) if shouldProcess
|
||||
else
|
||||
@highlightedWordManager.clearRows()
|
||||
for highlight in highlights
|
||||
@highlightedWordManager.addHighlight highlight
|
||||
|
||||
for misspelling in result.misspellings
|
||||
word = words[misspelling.index]
|
||||
position = positions[misspelling.index]
|
||||
@highlightedWordManager.addHighlight
|
||||
column: position.column
|
||||
row: position.row
|
||||
word: word
|
||||
suggestions: misspelling.suggestions
|
||||
if not words.length
|
||||
displayResult highlights
|
||||
else
|
||||
@apiRequest "/check", {language: language, words: words}, (error, result) =>
|
||||
if error? or !result? or !result.misspellings?
|
||||
return null
|
||||
mispelled = []
|
||||
for misspelling in result.misspellings
|
||||
word = words[misspelling.index]
|
||||
position = positions[misspelling.index]
|
||||
mispelled[misspelling.index] = true
|
||||
highlights.push
|
||||
column: position.column
|
||||
row: position.row
|
||||
word: word
|
||||
suggestions: misspelling.suggestions
|
||||
key = "#{language}:#{word}"
|
||||
if not seen[key]
|
||||
@cache.put key, misspelling.suggestions
|
||||
seen[key] = true
|
||||
for word, i in words when not mispelled[i]
|
||||
key = "#{language}:#{word}"
|
||||
if not seen[key]
|
||||
@cache.put(key, true)
|
||||
seen[key] = true
|
||||
displayResult highlights
|
||||
|
||||
getWords: (linesToProcess) ->
|
||||
lines = @editor.getValue().split("\n")
|
||||
|
|
|
@ -19,6 +19,12 @@ define [
|
|||
@recalculateDocList()
|
||||
|
||||
@_bindToSocketEvents()
|
||||
|
||||
@$scope.multiSelectedCount = 0
|
||||
|
||||
$(document).on "click", =>
|
||||
@clearMultiSelectedEntities()
|
||||
$scope.$digest()
|
||||
|
||||
_bindToSocketEvents: () ->
|
||||
@ide.socket.on "reciveNewDoc", (parent_folder_id, doc) =>
|
||||
|
@ -65,6 +71,7 @@ define [
|
|||
@$scope.$apply () =>
|
||||
@_deleteEntityFromScope entity
|
||||
@recalculateDocList()
|
||||
@$scope.$emit "entity:deleted", entity
|
||||
|
||||
@ide.socket.on "reciveEntityMove", (entity_id, folder_id) =>
|
||||
entity = @findEntityById(entity_id)
|
||||
|
@ -78,7 +85,62 @@ define [
|
|||
@ide.fileTreeManager.forEachEntity (entity) ->
|
||||
entity.selected = false
|
||||
entity.selected = true
|
||||
|
||||
toggleMultiSelectEntity: (entity) ->
|
||||
entity.multiSelected = !entity.multiSelected
|
||||
@$scope.multiSelectedCount = @multiSelectedCount()
|
||||
|
||||
multiSelectedCount: () ->
|
||||
count = 0
|
||||
@forEachEntity (entity) ->
|
||||
if entity.multiSelected
|
||||
count++
|
||||
return count
|
||||
|
||||
getMultiSelectedEntities: () ->
|
||||
entities = []
|
||||
@forEachEntity (e) ->
|
||||
if e.multiSelected
|
||||
entities.push e
|
||||
return entities
|
||||
|
||||
getMultiSelectedEntityChildNodes: () ->
|
||||
entities = @getMultiSelectedEntities()
|
||||
paths = {}
|
||||
for entity in entities
|
||||
paths[@getEntityPath(entity)] = entity
|
||||
prefixes = {}
|
||||
for path, entity of paths
|
||||
parts = path.split("/")
|
||||
if parts.length <= 1
|
||||
continue
|
||||
else
|
||||
# Record prefixes a/b/c.tex -> 'a' and 'a/b'
|
||||
for i in [1..(parts.length - 1)]
|
||||
prefixes[parts.slice(0,i).join("/")] = true
|
||||
child_entities = []
|
||||
for path, entity of paths
|
||||
# If the path is in the prefixes, then it's a parent folder and
|
||||
# should be ignore
|
||||
if !prefixes[path]?
|
||||
child_entities.push entity
|
||||
return child_entities
|
||||
|
||||
clearMultiSelectedEntities: () ->
|
||||
return if @$scope.multiSelectedCount == 0 # Be efficient, this is called a lot on 'click'
|
||||
@forEachEntity (entity) ->
|
||||
entity.multiSelected = false
|
||||
@$scope.multiSelectedCount = 0
|
||||
|
||||
multiSelectSelectedEntity: () ->
|
||||
@findSelectedEntity()?.multiSelected = true
|
||||
|
||||
existsInFolder: (folder_id, name) ->
|
||||
folder = @findEntityById(folder_id)
|
||||
return false if !folder?
|
||||
entity = @_findEntityByPathInFolder(folder, name)
|
||||
return entity?
|
||||
|
||||
findSelectedEntity: () ->
|
||||
selected = null
|
||||
@forEachEntity (entity) ->
|
||||
|
@ -277,7 +339,7 @@ define [
|
|||
deleteEntity: (entity, callback = (error) ->) ->
|
||||
# We'll wait for the socket.io notification to
|
||||
# delete from scope.
|
||||
return @ide.$http {
|
||||
return @ide.queuedHttp {
|
||||
method: "DELETE"
|
||||
url: "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}"
|
||||
headers:
|
||||
|
@ -289,7 +351,7 @@ define [
|
|||
# since that would break the tree structure.
|
||||
return if @_isChildFolder(entity, parent_folder)
|
||||
@_moveEntityInScope(entity, parent_folder)
|
||||
return @ide.$http.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", {
|
||||
return @ide.queuedHttp.post "/project/#{@ide.project_id}/#{entity.type}/#{entity.id}/move", {
|
||||
folder_id: parent_folder.id
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
|
@ -316,8 +378,6 @@ define [
|
|||
entity.deleted = true
|
||||
@$scope.deletedDocs.push entity
|
||||
|
||||
@$scope.$emit "entity:deleted", entity
|
||||
|
||||
_moveEntityInScope: (entity, parent_folder) ->
|
||||
return if entity in parent_folder.children
|
||||
@_deleteEntityFromScope(entity, moveToDeleted: false)
|
||||
|
|
|
@ -61,6 +61,8 @@ define [
|
|||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createDoc(name, parent_folder)
|
||||
.error (e)->
|
||||
$scope.error = e
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
|
@ -90,6 +92,8 @@ define [
|
|||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.createFolder(name, parent_folder)
|
||||
.error (e)->
|
||||
$scope.error = e
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
|
@ -99,17 +103,30 @@ define [
|
|||
]
|
||||
|
||||
App.controller "UploadFileModalController", [
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder) ->
|
||||
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window"
|
||||
($scope, ide, $modalInstance, $timeout, parent_folder, $window) ->
|
||||
$scope.parent_folder_id = parent_folder?.id
|
||||
$scope.tooManyFiles = false
|
||||
$scope.rateLimitHit = false
|
||||
$scope.secondsToRedirect = 10
|
||||
$scope.notLoggedIn = false
|
||||
$scope.conflicts = []
|
||||
$scope.control = {}
|
||||
|
||||
uploadCount = 0
|
||||
$scope.onUpload = () ->
|
||||
uploadCount++
|
||||
needToLogBackIn = ->
|
||||
$scope.notLoggedIn = true
|
||||
decreseTimeout = ->
|
||||
$timeout (() ->
|
||||
if $scope.secondsToRedirect == 0
|
||||
$window.location.href = "/login?redir=/project/#{ide.project_id}"
|
||||
else
|
||||
decreseTimeout()
|
||||
$scope.secondsToRedirect = $scope.secondsToRedirect - 1
|
||||
), 1000
|
||||
|
||||
$scope.max_files = 20
|
||||
decreseTimeout()
|
||||
|
||||
$scope.max_files = 40
|
||||
$scope.onComplete = (error, name, response) ->
|
||||
$timeout (() ->
|
||||
uploadCount--
|
||||
|
@ -127,8 +144,40 @@ define [
|
|||
return true
|
||||
|
||||
$scope.onError = (id, name, reason)->
|
||||
console.log(id, name, reason)
|
||||
if reason.indexOf("429") != -1
|
||||
$scope.rateLimitHit = true
|
||||
else if reason.indexOf("403") != -1
|
||||
needToLogBackIn()
|
||||
|
||||
_uploadTimer = null
|
||||
uploadIfNoConflicts = () ->
|
||||
if $scope.conflicts.length == 0
|
||||
$scope.doUpload()
|
||||
|
||||
uploadCount = 0
|
||||
$scope.onSubmit = (id, name) ->
|
||||
uploadCount++
|
||||
if ide.fileTreeManager.existsInFolder($scope.parent_folder_id, name)
|
||||
$scope.conflicts.push name
|
||||
$scope.$apply()
|
||||
if !_uploadTimer?
|
||||
_uploadTimer = setTimeout () ->
|
||||
_uploadTimer = null
|
||||
uploadIfNoConflicts()
|
||||
, 0
|
||||
return true
|
||||
|
||||
$scope.onCancel = (id, name) ->
|
||||
uploadCount--
|
||||
index = $scope.conflicts.indexOf(name)
|
||||
if index > -1
|
||||
$scope.conflicts.splice(index, 1)
|
||||
$scope.$apply()
|
||||
uploadIfNoConflicts()
|
||||
|
||||
$scope.doUpload = () ->
|
||||
$scope.control?.q?.uploadStoredFiles()
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
|
|
@ -2,9 +2,23 @@ define [
|
|||
"base"
|
||||
], (App) ->
|
||||
App.controller "FileTreeEntityController", ["$scope", "ide", "$modal", ($scope, ide, $modal) ->
|
||||
$scope.select = () ->
|
||||
ide.fileTreeManager.selectEntity($scope.entity)
|
||||
$scope.$emit "entity:selected", $scope.entity
|
||||
$scope.select = (e) ->
|
||||
if e.ctrlKey or e.metaKey
|
||||
e.stopPropagation()
|
||||
initialMultiSelectCount = ide.fileTreeManager.multiSelectedCount()
|
||||
ide.fileTreeManager.toggleMultiSelectEntity($scope.entity) == 0
|
||||
if initialMultiSelectCount == 0
|
||||
# On first multi selection, also include the current active/open file.
|
||||
ide.fileTreeManager.multiSelectSelectedEntity()
|
||||
else
|
||||
ide.fileTreeManager.selectEntity($scope.entity)
|
||||
$scope.$emit "entity:selected", $scope.entity
|
||||
|
||||
$scope.draggableHelper = () ->
|
||||
if ide.fileTreeManager.multiSelectedCount() > 0
|
||||
return $("<strong style='z-index:100'>#{ide.fileTreeManager.multiSelectedCount()} Files</strong>")
|
||||
else
|
||||
return $("<strong style='z-index:100'>#{$scope.entity.name}</strong>")
|
||||
|
||||
$scope.inputs =
|
||||
name: $scope.entity.name
|
||||
|
@ -24,10 +38,15 @@ define [
|
|||
$scope.startRenaming() if $scope.entity.selected
|
||||
|
||||
$scope.openDeleteModal = () ->
|
||||
if ide.fileTreeManager.multiSelectedCount() > 0
|
||||
entities = ide.fileTreeManager.getMultiSelectedEntityChildNodes()
|
||||
else
|
||||
entities = [$scope.entity]
|
||||
$modal.open(
|
||||
templateUrl: "deleteEntityModalTemplate"
|
||||
controller: "DeleteEntityModalController"
|
||||
scope: $scope
|
||||
resolve:
|
||||
entities: () -> entities
|
||||
)
|
||||
|
||||
$scope.$on "delete:selected", () ->
|
||||
|
@ -35,18 +54,18 @@ define [
|
|||
]
|
||||
|
||||
App.controller "DeleteEntityModalController", [
|
||||
"$scope", "ide", "$modalInstance",
|
||||
($scope, ide, $modalInstance) ->
|
||||
"$scope", "ide", "$modalInstance", "entities"
|
||||
($scope, ide, $modalInstance, entities) ->
|
||||
$scope.state =
|
||||
inflight: false
|
||||
|
||||
$scope.entities = entities
|
||||
|
||||
$scope.delete = () ->
|
||||
$scope.state.inflight = true
|
||||
ide.fileTreeManager
|
||||
.deleteEntity($scope.entity)
|
||||
.success () ->
|
||||
$scope.state.inflight = false
|
||||
$modalInstance.close()
|
||||
for entity in $scope.entities
|
||||
ide.fileTreeManager.deleteEntity(entity)
|
||||
$modalInstance.close()
|
||||
|
||||
$scope.cancel = () ->
|
||||
$modalInstance.dismiss('cancel')
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue