diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index e2db435299..7d49df07af 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -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: [ diff --git a/services/web/app.coffee b/services/web/app.coffee index a314885ccf..3d45307e5d 100644 --- a/services/web/app.coffee +++ b/services/web/app.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index c992fec578..312b441158 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -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' diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index 254c5c6c41..d815b426fe 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -1,4 +1,3 @@ -Settings = require 'settings-sharelatex' User = require("../../models/User").User {db, ObjectId} = require("../../infrastructure/mongojs") crypto = require 'crypto' diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 186d11d348..8f726602c3 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/services/web/app/coffee/Features/Blog/BlogController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index e03916c0fe..8b7ef023bd 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index 587f00647c..89fbc87e2e 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -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? diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index b7760b40c5..dcf0615b25 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -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? diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 4ac575af58..8cae627ab6 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index e2ca000474..38f788dd41 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 2619316e6d..c52d0f5c67 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 8d37f38c85..e14b9e4582 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -1,5 +1,4 @@ _ = require('underscore') - PersonalEmailLayout = require("./Layouts/PersonalEmailLayout") NotificationEmailLayout = require("./Layouts/NotificationEmailLayout") settings = require("settings-sharelatex") @@ -15,8 +14,6 @@ templates.registered =
Click here to set your password and log in.
-Once you have reset your password you can log in here.
-If you have any questions or problems, please contact #{settings.adminEmail}.
""" diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee index 680d7708c3..c5ff09b1b0 100644 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ b/services/web/app/coffee/Features/Email/EmailSender.coffee @@ -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) - diff --git a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee index 34ab6f8ece..09790cf99f 100644 --- a/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee +++ b/services/web/app/coffee/Features/FileStore/FileStoreHandler.coffee @@ -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" diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee new file mode 100644 index 0000000000..314142b3af --- /dev/null +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee new file mode 100644 index 0000000000..c4931f5327 --- /dev/null +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -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" diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee new file mode 100644 index 0000000000..a26bfff16a --- /dev/null +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -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 diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index d574a6838f..fb0f75beee 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee index f4bad9c593..4e67e9f1f4 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee @@ -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 \ No newline at end of file + callback null, true, user_id \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 67c863bde4..9a04fafd15 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -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 \ No newline at end of file diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index c461c04341..a21dfc7f9e 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 6fdba733e7..cdbc933135 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index a0f8cca509..3b3233157d 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee index 69f9eb309a..bf775caeeb 100644 --- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 338c660c0c..f12a7d548e 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -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" diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index f78a638c14..9b771625b3 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -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' diff --git a/services/web/app/coffee/Features/Project/ProjectGetter.coffee b/services/web/app/coffee/Features/Project/ProjectGetter.coffee index 129ff831dc..d8bb1e457a 100644 --- a/services/web/app/coffee/Features/Project/ProjectGetter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectGetter.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index 998e2fc60b..7d1eb8f749 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -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) diff --git a/services/web/app/coffee/Features/References/ReferencesController.coffee b/services/web/app/coffee/Features/References/ReferencesController.coffee new file mode 100644 index 0000000000..0536e3b680 --- /dev/null +++ b/services/web/app/coffee/Features/References/ReferencesController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee new file mode 100644 index 0000000000..0b2ddb1e26 --- /dev/null +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -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) + ) diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee index 19b8c15648..dc71da09fc 100644 --- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee +++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee index 81a697525f..86683e2341 100644 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -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) diff --git a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee b/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee index f1079cc5b7..f1f55814c7 100644 --- a/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee +++ b/services/web/app/coffee/Features/StaticPages/StaticPagesRouter.coffee @@ -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") diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index f0513fb590..d323c19a9d 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -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) -> diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee index ed7813d5c7..680905ca6c 100644 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -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) -> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index 4fbd84a6ac..e2e021bd97 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -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 = "" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee index b57c3492f6..a71d4496e7 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionDomainHandler.coffee @@ -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 - diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 3bf8d4a631..af8b2414f3 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index ffd436943a..1f078f6674 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -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)-> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee index 7fa43f4270..2396dca07b 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index 08f5507b7f..9285f8e575 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index 76dded3f4d..f2d66c30c5 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 7440657c7c..18f9058b67 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee index 714396b1f5..bdb9c41fde 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee index 2d84d69b10..c0b691e677 100644 --- a/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/UserFeaturesUpdater.coffee @@ -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 diff --git a/services/web/app/coffee/Features/Tags/TagsController.coffee b/services/web/app/coffee/Features/Tags/TagsController.coffee index aeef36ed47..2e67be2fd4 100644 --- a/services/web/app/coffee/Features/Tags/TagsController.coffee +++ b/services/web/app/coffee/Features/Tags/TagsController.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Tags/TagsHandler.coffee b/services/web/app/coffee/Features/Tags/TagsHandler.coffee index 18b9608152..2dc57608bd 100644 --- a/services/web/app/coffee/Features/Tags/TagsHandler.coffee +++ b/services/web/app/coffee/Features/Tags/TagsHandler.coffee @@ -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" diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee index f256760808..6801c6b0b3 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsController.coffee @@ -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) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee index 7cd0e53284..7605f6911b 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee @@ -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)-> diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee index f2d5bb94d1..521aabeb0c 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateSender.coffee @@ -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) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index d18a8713ab..212c251029 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -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)-> diff --git a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee index e666b83f24..a615810fe6 100644 --- a/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ArchiveManager.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee index 4f0c4dcc0b..04c7c3bcc7 100644 --- a/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee +++ b/services/web/app/coffee/Features/Uploads/FileSystemImportManager.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee index 83a8db5c2d..b7cab7ac5c 100644 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee @@ -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? diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee index b5d108d21d..127c6bc94f 100644 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadManager.coffee @@ -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()}") diff --git a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee index 63ad6eb96d..c82144acc0 100644 --- a/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee +++ b/services/web/app/coffee/Features/Uploads/UploadsRouter.coffee @@ -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 diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 94dc811e99..451729d7e9 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -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" diff --git a/services/web/app/coffee/Features/User/UserHandler.coffee b/services/web/app/coffee/Features/User/UserHandler.coffee new file mode 100644 index 0000000000..8af78573d6 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserHandler.coffee @@ -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 + diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 6ffb8ecdb8..5a967605fe 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -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" diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index 7dfde21c8a..f407091122 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -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 diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 61eaf6ef6d..1fd1aa10a2 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -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}" diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 111ef40cfb..dc9f0927fd 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -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++ diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index af83318b49..8fca181d0e 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -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 } diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index a0c1b50d21..46b1db2157 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -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 \ No newline at end of file diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 87fe3e7b8e..e666724b09 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -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'). diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 2241eb3cd2..f79f6e3c19 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -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", diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 31564b0db1..bb07ee9e88 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -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" diff --git a/services/web/app/views/project/editor/hotkeys.jade b/services/web/app/views/project/editor/hotkeys.jade index c584e5eb28..c2c5e66759 100644 --- a/services/web/app/views/project/editor/hotkeys.jade +++ b/services/web/app/views/project/editor/hotkeys.jade @@ -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()" diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 28d78480fe..5d58f64e13 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -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 diff --git a/services/web/app/views/project/editor/publish-template.jade b/services/web/app/views/project/editor/publish-template.jade index 7fd2c1a0f4..c89aeebe6d 100644 --- a/services/web/app/views/project/editor/publish-template.jade +++ b/services/web/app/views/project/editor/publish-template.jade @@ -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")}... \ No newline at end of file + 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")} diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index 06aa611725..dad1d42108 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -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 diff --git a/services/web/app/views/project/editor/track-changes.jade b/services/web/app/views/project/editor/track-changes.jade index 3f0bd539b0..dc7ce38708 100644 --- a/services/web/app/views/project/editor/track-changes.jade +++ b/services/web/app/views/project/editor/track-changes.jade @@ -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")} diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 54b2b2f42f..1c80b9a7a2 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -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") diff --git a/services/web/app/views/project/list/modals.jade b/services/web/app/views/project/list/modals.jade index ca609ae697..e0fe24ad14 100644 --- a/services/web/app/views/project/list/modals.jade +++ b/services/web/app/views/project/list/modals.jade @@ -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 diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade new file mode 100644 index 0000000000..29f371b724 --- /dev/null +++ b/services/web/app/views/project/list/notifications.jade @@ -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")} diff --git a/services/web/app/views/project/list/project-list.jade b/services/web/app/views/project/list/project-list.jade index 41f9b03bcc..9e8c22a1e8 100644 --- a/services/web/app/views/project/list/project-list.jade +++ b/services/web/app/views/project/list/project-list.jade @@ -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'") diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade index c10914719f..a30127840a 100644 --- a/services/web/app/views/project/list/side-bar.jade +++ b/services/web/app/views/project/list/side-bar.jade @@ -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} diff --git a/services/web/app/views/sentry.jade b/services/web/app/views/sentry.jade index d1c6115030..9fe5dd3b7d 100644 --- a/services/web/app/views/sentry.jade +++ b/services/web/app/views/sentry.jade @@ -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) { diff --git a/services/web/app/views/subscriptions/custom_account.jade b/services/web/app/views/subscriptions/custom_account.jade index 53cf86f3af..483517d873 100644 --- a/services/web/app/views/subscriptions/custom_account.jade +++ b/services/web/app/views/subscriptions/custom_account.jade @@ -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) diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade index 06f801a143..dd3e600aa1 100644 --- a/services/web/app/views/subscriptions/dashboard.jade +++ b/services/web/app/views/subscriptions/dashboard.jade @@ -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: "" + groupSubscription.admin_id.email + ""})} + 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:"" + subscription.expiresAt + ""})} - p !{translate("choose_a_plan_below")} + when "active" p !{translate("currently_subscribed_to_plan", {planName:"" + subscription.name + ""})} a(href, ng-click="changePlan = true") !{translate("change_plan")}. p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"" + subscription.price + "", collectionDate:"" + subscription.nextPaymentDueAt + ""})} 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:"" + subscription.name + ""})} p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate:"" + subscription.nextPaymentDueAt + ""})} @@ -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:"{{plan.name}}"})} .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")}... + + diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade index 564d356b2b..b70aec29fc 100644 --- a/services/web/app/views/subscriptions/new.jade +++ b/services/web/app/views/subscriptions/new.jade @@ -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 diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade index f085c5b5c1..d451c94540 100644 --- a/services/web/app/views/subscriptions/plans.jade +++ b/services/web/app/views/subscriptions/plans.jade @@ -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 diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.jade index 8b66dec6c9..86b0263238 100644 --- a/services/web/app/views/subscriptions/successful_subscription.jade +++ b/services/web/app/views/subscriptions/successful_subscription.jade @@ -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:""+subscription.price+"", collectionDate:""+subscription.nextPaymentDueAt+""})} - 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")} diff --git a/services/web/app/views/user/activate.jade b/services/web/app/views/user/activate.jade new file mode 100644 index 0000000000..7961876389 --- /dev/null +++ b/services/web/app/views/user/activate.jade @@ -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 || {})} diff --git a/services/web/app/views/user/login.jade b/services/web/app/views/user/login.jade index 4d8848d8a6..a6587782bf 100644 --- a/services/web/app/views/user/login.jade +++ b/services/web/app/views/user/login.jade @@ -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") diff --git a/services/web/app/views/user/setPassword.jade b/services/web/app/views/user/setPassword.jade index 9da2beb065..3a6a588f34 100644 --- a/services/web/app/views/user/setPassword.jade +++ b/services/web/app/views/user/setPassword.jade @@ -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( diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 73677b2ecc..3f73439cd0 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -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 ShareLaTeX © 2015" + text: "Powered by ShareLaTeX © 2016" }] right_footer: [{ diff --git a/services/web/package.json b/services/web/package.json index 8f31c75b7f..23ac125ef1 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -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", diff --git a/services/web/public/coffee/directives/fineUpload.coffee b/services/web/public/coffee/directives/fineUpload.coffee index 2fad1acf17..92cdf720f4 100644 --- a/services/web/public/coffee/directives/fineUpload.coffee +++ b/services/web/public/coffee/directives/fineUpload.coffee @@ -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: """