diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index a90ab5504a..a2444d34d7 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -176,7 +176,7 @@ module.exports = EditorController = callback?() renameProject: (project_id, newName, callback = (err) ->) -> - ProjectDetailsHandler.renameProject project_id, newName, -> + ProjectDetailsHandler.renameProject project_id, newName, (err) -> if err? logger.err err:err, project_id:project_id, newName:newName, "error renaming project" return callback(err) diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index 45a743f282..55006b73c7 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -25,6 +25,18 @@ module.exports = ErrorController = else if error instanceof Errors.TooManyRequestsError logger.warn {err: error, url: req.url}, "too many requests error" res.sendStatus(429) + else if error instanceof Errors.InvalidNameError + logger.warn {err: error, url: req.url}, "invalid name error" + res.status(400) + res.send(error.message) else logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear" ErrorController.serverError req, res + + handleApiError: (error, req, res, next) -> + if error instanceof Errors.NotFoundError + logger.warn {err: error, url: req.url}, "not found error" + res.sendStatus(404) + else + logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear" + res.sendStatus(500) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 56c0ada7d5..2e46dd692d 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -5,7 +5,6 @@ NotFoundError = (message) -> return error NotFoundError.prototype.__proto__ = Error.prototype - ServiceNotConfiguredError = (message) -> error = new Error(message) error.name = "ServiceNotConfiguredError" @@ -13,7 +12,6 @@ ServiceNotConfiguredError = (message) -> return error ServiceNotConfiguredError.prototype.__proto__ = Error.prototype - TooManyRequestsError = (message) -> error = new Error(message) error.name = "TooManyRequestsError" @@ -21,8 +19,15 @@ TooManyRequestsError = (message) -> return error TooManyRequestsError.prototype.__proto__ = Error.prototype +InvalidNameError = (message) -> + error = new Error(message) + error.name = "InvalidNameError" + error.__proto__ = InvalidNameError.prototype + return error +InvalidNameError.prototype.__proto__ = Error.prototype module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError TooManyRequestsError: TooManyRequestsError + InvalidNameError: InvalidNameError diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index f967a98ffb..92750a5912 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -101,7 +101,7 @@ module.exports = ProjectController = res.send(project_id:project._id) - newProject: (req, res)-> + newProject: (req, res, next)-> user_id = AuthenticationController.getLoggedInUserId(req) projectName = req.body.projectName?.trim() template = req.body.template @@ -113,25 +113,17 @@ module.exports = ProjectController = else projectCreationHandler.createBasicProject user_id, projectName, cb ], (err, project)-> - if err? - logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project" - res.sendStatus 500 - else - logger.log project: project, user: user_id, name: projectName, templateType: template, "created project" - res.send {project_id:project._id} + return next(err) if err? + logger.log project: project, user: user_id, name: projectName, templateType: template, "created project" + res.send {project_id:project._id} - renameProject: (req, res)-> + renameProject: (req, res, next)-> project_id = req.params.Project_id newName = req.body.newProjectName - if newName.length > 150 - return res.sendStatus 400 editorController.renameProject project_id, newName, (err)-> - if err? - logger.err err:err, project_id:project_id, newName:newName, "problem renaming project" - res.sendStatus 500 - else - res.sendStatus 200 + return next(err) if err? + res.sendStatus 200 projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee index ffb69abeac..41b40c11b9 100644 --- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee @@ -6,6 +6,7 @@ ObjectId = require('mongoose').Types.ObjectId Project = require('../../models/Project').Project Folder = require('../../models/Folder').Folder ProjectEntityHandler = require('./ProjectEntityHandler') +ProjectDetailsHandler = require('./ProjectDetailsHandler') User = require('../../models/User').User fs = require('fs') Path = require "path" @@ -15,19 +16,21 @@ module.exports = ProjectCreationHandler = createBlankProject : (owner_id, projectName, callback = (error, project) ->)-> metrics.inc("project-creation") - logger.log owner_id:owner_id, projectName:projectName, "creating blank project" - rootFolder = new Folder {'name':'rootFolder'} - 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 - project.save (err)-> - return callback(err) if err? - callback err, project + ProjectDetailsHandler.validateProjectName projectName, (error) -> + return callback(error) if error? + logger.log owner_id:owner_id, projectName:projectName, "creating blank project" + rootFolder = new Folder {'name':'rootFolder'} + 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 + project.save (err)-> + return callback(err) if err? + callback err, project createBasicProject : (owner_id, projectName, callback = (error, project) ->)-> self = @ diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index 9c823c9f17..8234907ff4 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -7,8 +7,7 @@ _ = require("underscore") PublicAccessLevels = require("../Authorization/PublicAccessLevels") Errors = require("../Errors/Errors") -module.exports = - +module.exports = ProjectDetailsHandler = getDetails: (project_id, callback)-> ProjectGetter.getProject project_id, {name:true, description:true, compiler:true, features:true, owner_ref:true}, (err, project)-> if err? @@ -39,16 +38,29 @@ module.exports = callback(err) renameProject: (project_id, newName, callback = ->)-> - logger.log project_id: project_id, newName:newName, "renaming 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) - oldProjectName = project.name - Project.update _id:project_id, {name: newName}, (err, project)=> - if err? + ProjectDetailsHandler.validateProjectName newName, (error) -> + return callback(error) if error? + logger.log project_id: project_id, newName:newName, "renaming 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) - tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback + oldProjectName = project.name + Project.update _id:project_id, {name: newName}, (err, project)=> + if err? + return callback(err) + tpdsUpdateSender.moveEntity {project_id:project_id, project_name:oldProjectName, newProjectName:newName}, callback + + MAX_PROJECT_NAME_LENGTH: 150 + validateProjectName: (name, callback = (error) ->) -> + if name.length == 0 + return callback(new Errors.InvalidNameError("Project name cannot be blank")) + else if name.length > @MAX_PROJECT_NAME_LENGTH + return callback(new Errors.InvalidNameError("Project name is too long")) + else if name.indexOf("/") > -1 + return callback(new Errors.InvalidNameError("Project name cannot not contain / characters")) + else + return callback() setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> logger.log project_id: project_id, level: newAccessLevel, "set public access level" diff --git a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee index 7738425c8e..ba3b3b6295 100644 --- a/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectUpdateHandler.coffee @@ -1,6 +1,5 @@ Project = require('../../models/Project').Project logger = require('logger-sharelatex') -Project = require("../../models/Project").Project module.exports = markAsUpdated : (project_id, callback)-> diff --git a/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee b/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee index 0c858af75a..2e22ed5665 100644 --- a/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee +++ b/services/web/app/coffee/Features/Uploads/FileTypeManager.coffee @@ -3,7 +3,7 @@ Path = require("path") module.exports = FileTypeManager = TEXT_EXTENSIONS : [ - "tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md" + "tex", "latex", "sty", "cls", "bst", "bib", "bibtex", "txt", "tikz", "rtex", "md", "asy" ] IGNORE_EXTENSIONS : [ diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index b9ef1b7b31..4c031a8bd1 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -164,11 +164,12 @@ server = require('http').createServer(app) # process api routes first, if nothing matched fall though and use # web middlewear + routes app.use(apiRouter) +app.use(ErrorController.handleApiError) app.use(webRouter) +app.use(ErrorController.handleError) router = new Router(webRouter, apiRouter) -app.use ErrorController.handleError module.exports = app: app diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 4e86b31620..831dc212ae 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -167,6 +167,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate') .modal-header h3 #{translate("copy_project")} .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} form(name="cloneProjectForm", novalidate) .form-group label #{translate("new_name")} diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 9111b2a347..01cd71bca4 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -98,6 +98,8 @@ script(type='text/ng-template', id='renameProjectModalTemplate') ) × h3 #{translate("rename_project")} .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} form(name="renameProjectForm", novalidate) input.form-control( type="text", @@ -111,8 +113,10 @@ script(type='text/ng-template', id='renameProjectModalTemplate') button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} button.btn.btn-primary( ng-click="rename()", - ng-disabled="renameProjectForm.$invalid" - ) #{translate("rename")} + ng-disabled="renameProjectForm.$invalid || state.inflight" + ) + span(ng-show="!state.inflight") #{translate("rename")} + span(ng-show="state.inflight") #{translate("renaming")}... script(type='text/ng-template', id='cloneProjectModalTemplate') .modal-header @@ -123,6 +127,8 @@ script(type='text/ng-template', id='cloneProjectModalTemplate') ) × h3 #{translate("copy_project")} .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} form(name="cloneProjectForm", novalidate) .form-group label #{translate("new_name")} @@ -155,6 +161,8 @@ script(type='text/ng-template', id='newProjectModalTemplate') ) × h3 #{translate("new_project")} .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} form(novalidate, name="newProjectForm") input.form-control( type="text", diff --git a/services/web/app/views/sentry.pug b/services/web/app/views/sentry.pug index 9e10d7837e..6406302e0e 100644 --- a/services/web/app/views/sentry.pug +++ b/services/web/app/views/sentry.pug @@ -1,75 +1,78 @@ -- if (typeof(sentrySrc) != "undefined") - - if (sentrySrc.match(/^([a-z]+:)?\/\//i)) - script(src=sentrySrc) - - else - script(src=buildJsPath("libs/"+sentrySrc, {fingerprint:false})) -- if (typeof(sentrySrc) != "undefined") +- if (typeof(sentryPublicDSN) != "undefined") script(type="text/javascript"). - if (typeof(Raven) != "undefined" && Raven.config) { - Raven.config("#{sentryPublicDSN}", { - tags: { 'commit': '@@COMMIT@@', 'build': '@@RELEASE@@' }, - release: '@@RELEASE@@', - // Ignore list based off: https://gist.github.com/1878283 - ignoreErrors: [ - 'DealPly', - // Random plugins/extensions - 'top.GLOBALS', - // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html - 'originalCreateNotification', - 'canvas.contentDocument', - 'MyApp_RemoveAllHighlights', - 'http://tt.epicplay.com', - 'Can\'t find variable: ZiteReader', - 'jigsaw is not defined', - 'ComboSearch is not defined', - 'http://loading.retry.widdit.com/', - 'atomicFindClose', - // Facebook borked - 'fb_xd_fragment', - // ISP optimizing proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha) - // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy - 'bmi_SafeAddOnload', - 'EBCallBackMessageReceived', - // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx - 'conduitPage', - '/NS_ERROR_NOT_CONNECTED/i', - "/Cannot read property 'row' of undefined/i", - 'TypeError: start is undefined' - ], - ignoreUrls: [ - // Facebook flakiness - /graph\.facebook\.com/i, - // Facebook blocked - /connect\.facebook\.net\/en_US\/all\.js/i, - // Woopra flakiness - /eatdifferent\.com\.woopra-ns\.com/i, - /static\.woopra\.com\/js\/woopra\.js/i, - // Chrome extensions - /extensions\//i, - /^chrome:\/\//i, - // Other plugins - /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb - /webappstoolbarba\.texthelp\.com\//i, - /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, - /a\.disquscdn\.com/i, - /platform\.twitter\.com/i, - /pstatic\.datafastguru\.info/i - ], - shouldSendCallback: function(data) { - // only send a fraction of errors - var sampleRate = 0.01; - return (Math.random() <= sampleRate); - }, - dataCallback: function(data) { - // remove circular references from object - var cache = []; - var s = JSON.stringify(data, function(k, v) { if (typeof v === 'object' && v !== null) { if (cache.indexOf(v) !== -1) return "[circular]"; cache.push(v); }; return v; }); - return JSON.parse(s); + require.config({ + paths: { + 'raven': 'libs/raven-3.15.0.min' } - // we highly recommend restricting exceptions to a domain in order to filter out clutter - // whitelistUrls: ['example.com/scripts/'] - }).install(); - } + }); + + require(["raven"], function(Raven) { + if (typeof(Raven) != "undefined" && Raven.config) { + Raven.config("#{sentryPublicDSN}", { + tags: { 'commit': '@@COMMIT@@', 'build': '@@RELEASE@@' }, + release: '@@RELEASE@@', + // Ignore list based off: https://gist.github.com/1878283 + ignoreErrors: [ + 'DealPly', + // Random plugins/extensions + 'top.GLOBALS', + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + 'Can\'t find variable: ZiteReader', + 'jigsaw is not defined', + 'ComboSearch is not defined', + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + // Facebook borked + 'fb_xd_fragment', + // ISP optimizing proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha) + // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage', + '/NS_ERROR_NOT_CONNECTED/i', + "/Cannot read property 'row' of undefined/i", + 'TypeError: start is undefined' + ], + ignoreUrls: [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, + /a\.disquscdn\.com/i, + /platform\.twitter\.com/i, + /pstatic\.datafastguru\.info/i + ], + shouldSendCallback: function(data) { + // only send a fraction of errors + var sampleRate = 0.01; + return (Math.random() <= sampleRate); + }, + dataCallback: function(data) { + // remove circular references from object + var cache = []; + var s = JSON.stringify(data, function(k, v) { if (typeof v === 'object' && v !== null) { if (cache.indexOf(v) !== -1) return "[circular]"; cache.push(v); }; return v; }); + return JSON.parse(s); + } + // we highly recommend restricting exceptions to a domain in order to filter out clutter + // whitelistUrls: ['example.com/scripts/'] + }).install(); + } + }) - if (user && typeof(user) != "undefined" && typeof (user.email) != "undefined") script(type="text/javascript"). if (typeof(Raven) != "undefined" && Raven.setUserContext) { diff --git a/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee b/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee index 5c7cc55ea3..c0feaea97a 100644 --- a/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee +++ b/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee @@ -6,6 +6,7 @@ define [ projectName: ide.$scope.project.name + " (Copy)" $scope.state = inflight: false + error: false $modalInstance.opened.then () -> $timeout () -> @@ -20,9 +21,16 @@ define [ $scope.clone = () -> $scope.state.inflight = true + $scope.state.error = false cloneProject($scope.inputs.projectName) - .then (data) -> - window.location = "/project/#{data.data.project_id}" + .success (data) -> + window.location = "/project/#{data.project_id}" + .error (body, statusCode) -> + $scope.state.inflight = false + if statusCode == 400 + $scope.state.error = { message: body } + else + $scope.state.error = true $scope.cancel = () -> $modalInstance.dismiss('cancel') \ No newline at end of file diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index a321b85049..d1aa883734 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -35,6 +35,7 @@ define [ @doc?.attachToAce(@ace) editorDoc = @ace.getSession().getDocument() editorDoc.on "change", @_checkConsistency + @ide.$scope.$emit 'document:opened', @doc detachFromAce: () -> @doc?.detachFromAce() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index b8cfe53806..377d5c0103 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -9,7 +9,8 @@ define [ "ide/editor/directives/aceEditor/highlights/HighlightsManager" "ide/editor/directives/aceEditor/cursor-position/CursorPositionManager" "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" -], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) -> + "ide/editor/directives/aceEditor/labels/LabelsManager" +], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') @@ -84,7 +85,6 @@ define [ scope.name = attrs.aceEditor - autoCompleteManager = new AutoCompleteManager(scope, editor, element) if scope.spellCheck # only enable spellcheck when explicitly required spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000}) spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache) @@ -92,6 +92,8 @@ define [ highlightsManager = new HighlightsManager(scope, editor, element) cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) trackChangesManager = new TrackChangesManager(scope, editor, element) + labelsManager = new LabelsManager(scope, editor, element) + autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager) # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index c5b37d1f3b..e2ef4cd03e 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -13,7 +13,7 @@ define [ return null class AutoCompleteManager - constructor: (@$scope, @editor) -> + constructor: (@$scope, @editor, @element, @labelsManager) -> @suggestionManager = new SuggestionManager() @monkeyPatchAutocomplete() @@ -40,6 +40,35 @@ define [ SnippetCompleter = new SnippetManager() + labelsManager = @labelsManager + LabelsCompleter = + getCompletions: (editor, session, pos, prefxi, callback) -> + upToCursorRange = new Range(pos.row, 0, pos.row, pos.column) + lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) + commandFragment = getLastCommandFragment(lineUpToCursor) + if commandFragment + refMatch = commandFragment.match(/^~?\\ref{([^}]*, *)?(\w*)/) + if refMatch + beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) + lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) + needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/) + currentArg = refMatch[1] + result = [] + result.push { + caption: "\\ref{}", + snippet: "\\ref{}", + meta: "cross-reference", + score: 11000 + } + for label in labelsManager.getAllLabels() + result.push { + caption: "\\ref{#{label}#{if needsClosingBrace then '}' else ''}", + value: "\\ref{#{label}#{if needsClosingBrace then '}' else ''}", + meta: "cross-reference", + score: 10000 + } + callback null, result + references = @$scope.$root._references ReferencesCompleter = getCompletions: (editor, session, pos, prefix, callback) -> @@ -78,7 +107,7 @@ define [ else callback null, result - @editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter] + @editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter, LabelsCompleter] disable: () -> @editor.setOptions({ @@ -89,18 +118,21 @@ define [ onChange: (change) -> cursorPosition = @editor.getCursorPosition() end = change.end + range = new Range(end.row, 0, end.row, end.column) + lineUpToCursor = @editor.getSession().getTextRange(range) + commandFragment = getLastCommandFragment(lineUpToCursor) # Check that this change was made by us, not a collaborator # (Cursor is still one place behind) - if end.row == cursorPosition.row and end.column == cursorPosition.column + 1 - if change.action == "insert" - range = new Range(end.row, 0, end.row, end.column) - lineUpToCursor = @editor.getSession().getTextRange(range) - commandFragment = getLastCommandFragment(lineUpToCursor) - - if commandFragment? and commandFragment.length > 2 - setTimeout () => - @editor.execCommand("startAutocomplete") - , 0 + # NOTE: this is also the case when a user backspaces over a highlighted region + if ( + change.action == "insert" and + end.row == cursorPosition.row and + end.column == cursorPosition.column + 1 + ) + if commandFragment? and commandFragment.length > 2 + setTimeout () => + @editor.execCommand("startAutocomplete") + , 0 monkeyPatchAutocomplete: () -> Autocomplete = ace.require("ace/autocomplete").Autocomplete diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee new file mode 100644 index 0000000000..0e504b4ff8 --- /dev/null +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee @@ -0,0 +1,64 @@ +define [ + "ace/ace" +], () -> + Range = ace.require("ace/range").Range + + getLastCommandFragment = (lineUpToCursor) -> + if m = lineUpToCursor.match(/(\\[^\\]+)$/) + return m[1] + else + return null + + class LabelsManager + constructor: (@$scope, @editor, @element) -> + + @state = + documents: {} # map of DocId => List[Label] + + @loadLabelsTimeout = null + + onChange = (change) => + cursorPosition = @editor.getCursorPosition() + end = change.end + range = new Range(end.row, 0, end.row, end.column) + lineUpToCursor = @editor.getSession().getTextRange(range) + commandFragment = getLastCommandFragment(lineUpToCursor) + if ( + change.action in ['remove', 'insert'] and + ((_.any(change.lines, (line) -> line.match(/\\label\{[^\}\n\\]{0,80}\}/))) or + (commandFragment?.length > 2 and commandFragment.slice(0,7) == '\\label{')) + ) + @scheduleLoadLabelsFromOpenDoc() + + @editor.on "changeSession", (e) => + e.oldSession.off "change", onChange + e.session.on "change", onChange + setTimeout( + () => + @scheduleLoadLabelsFromOpenDoc() + , 0 + ) + + loadLabelsFromOpenDoc: () -> + docId = @$scope.docId + docText = @editor.getValue() + labels = [] + re = /\\label\{([^\}\n\\]{0,80})\}/g + while (labelMatch = re.exec(docText)) and labels.length < 1000 + if labelMatch[1] + labels.push(labelMatch[1]) + @state.documents[docId] = labels + + scheduleLoadLabelsFromOpenDoc: () -> + # De-bounce loading labels with a timeout + if @loadLabelsTimeout + clearTimeout(@loadLabelsTimeout) + @loadLabelsTimeout = setTimeout( + () => + @loadLabelsFromOpenDoc() + , 1000 + , this + ) + + getAllLabels: () -> + _.flatten(labels for docId, labels of @state.documents) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 842d27ac39..12417d2e09 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -214,6 +214,59 @@ define [ rejectChangeIds: (change_ids) -> changes = @rangesTracker.getChanges(change_ids) return if changes.length == 0 + + # When doing bulk rejections, adjacent changes might interact with each other. + # Consider an insertion with an adjacent deletion (which is a common use-case, replacing words): + # + # "foo bar baz" -> "foo quux baz" + # + # The change above will be modeled with two ops, with the insertion going first: + # + # foo quux baz + # |--| -> insertion of "quux", op 1, at position 4 + # | -> deletion of "bar", op 2, pushed forward by "quux" to position 8 + # + # When rejecting these changes at once, if the insertion is rejected first, we get unexpected + # results. What happens is: + # + # 1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars + # starting from position 4; + # + # "foo quux baz" -> "foo baz" + # |--| -> 4 characters to be removed + # + # 2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if + # the word "quuux" was still present). + # + # "foo baz" -> "foo bazbar" + # | -> deletion of "bar" is reverted by reinserting "bar" at position 8 + # + # While the intended result would be "foo bar baz", what we get is: + # + # "foo bazbar" (note "bar" readded at position 8) + # + # The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted + # from position 4. This includes the position where the deletion exists; when that position is + # cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it. + # As we still hold a reference to it, the code tries to revert it by readding the deleted text, but + # does so at the outdated position (position 8, which was valid when "quux" was present). + # + # To avoid this kind of problem, we need to make sure that reverting operations doesn't affect + # subsequent operations that come after. Reverse sorting the operations based on position will + # achieve it; in the case above, it makes sure that the the deletion is reverted first: + # + # 1) Rejecting the deletion adds the deleted word "bar" at position 8 + # + # "foo quux baz" -> "foo quuxbar baz" + # | -> deletion of "bar" is reverted by + # reinserting "bar" at position 8 + # + # 2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars + # starting from position 4 and achieves the expected result: + # + # "foo quuxbar baz" -> "foo bar baz" + # |--| -> 4 characters to be removed + changes.sort((a, b) -> b.op.p - a.op.p) session = @editor.getSession() diff --git a/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee b/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee index 1cb6644bed..2163bbc8cb 100644 --- a/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee @@ -19,12 +19,18 @@ define [ $scope.finishRenaming = () -> $scope.state.renaming = false newName = $scope.inputs.name - if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH - return if $scope.project.name == newName return + oldName = $scope.project.name $scope.project.name = newName settings.saveProjectSettings({name: $scope.project.name}) + .error (response, statusCode) -> + $scope.project.name = oldName + if statusCode == 400 + ide.showGenericMessageModal("Error renaming project", response) + else + ide.showGenericMessageModal("Error renaming project", "Please try again in a moment") + console.log arguments ide.socket.on "projectNameUpdated", (name) -> $scope.$apply () -> diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 04a9ccb5e3..f653359d17 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -12,8 +12,7 @@ define [ # End of tracking code. data._csrf = window.csrfToken - ide.$http.post "/user/settings", data - + return ide.$http.post "/user/settings", data saveProjectSettings: (data) -> # Tracking code. @@ -24,9 +23,8 @@ define [ # End of tracking code. data._csrf = window.csrfToken - ide.$http.post "/project/#{ide.project_id}/settings", data + return ide.$http.post "/project/#{ide.project_id}/settings", data - saveProjectAdminSettings: (data) -> # Tracking code. for key in Object.keys(data) @@ -36,8 +34,6 @@ define [ # End of tracking code. data._csrf = window.csrfToken - ide.$http.post "/project/#{ide.project_id}/settings/admin", data - - + return ide.$http.post "/project/#{ide.project_id}/settings/admin", data } ] \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/modal-controllers.coffee b/services/web/public/coffee/main/project-list/modal-controllers.coffee index dbf3c613ac..4e2285a4de 100644 --- a/services/web/public/coffee/main/project-list/modal-controllers.coffee +++ b/services/web/public/coffee/main/project-list/modal-controllers.coffee @@ -1,9 +1,13 @@ define [ "base" ], (App) -> - App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, projectName) -> + App.controller 'RenameProjectModalController', ($scope, $modalInstance, $timeout, project, queuedHttp) -> $scope.inputs = - projectName: projectName + projectName: project.name + + $scope.state = + inflight: false + error: false $modalInstance.opened.then () -> $timeout () -> @@ -11,7 +15,20 @@ define [ , 200 $scope.rename = () -> - $modalInstance.close($scope.inputs.projectName) + $scope.state.inflight = true + $scope.state.error = false + $scope + .renameProject(project, $scope.inputs.projectName) + .success () -> + $scope.state.inflight = false + $scope.state.error = false + $modalInstance.close() + .error (body, statusCode) -> + $scope.state.inflight = false + if statusCode == 400 + $scope.state.error = { message: body } + else + $scope.state.error = true $scope.cancel = () -> $modalInstance.dismiss('cancel') @@ -21,6 +38,7 @@ define [ projectName: project.name + " (Copy)" $scope.state = inflight: false + error: false $modalInstance.opened.then () -> $timeout () -> @@ -31,9 +49,16 @@ define [ $scope.state.inflight = true $scope .cloneProject(project, $scope.inputs.projectName) - .then (project_id) -> + .success () -> $scope.state.inflight = false - $modalInstance.close(project_id) + $scope.state.error = false + $modalInstance.close() + .error (body, statusCode) -> + $scope.state.inflight = false + if statusCode == 400 + $scope.state.error = { message: body } + else + $scope.state.error = true $scope.cancel = () -> $modalInstance.dismiss('cancel') @@ -43,6 +68,7 @@ define [ projectName: "" $scope.state = inflight: false + error: false $modalInstance.opened.then () -> $timeout () -> @@ -51,11 +77,19 @@ define [ $scope.create = () -> $scope.state.inflight = true + $scope.state.error = false $scope .createProject($scope.inputs.projectName, template) - .then (project_id) -> + .success (data) -> $scope.state.inflight = false - $modalInstance.close(project_id) + $scope.state.error = false + $modalInstance.close(data.project_id) + .error (body, statusCode) -> + $scope.state.inflight = false + if statusCode == 400 + $scope.state.error = { message: body } + else + $scope.state.error = true $scope.cancel = () -> $modalInstance.dismiss('cancel') diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index ad9d0f3582..1ce584fe8b 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -256,9 +256,7 @@ define [ ) $scope.createProject = (name, template = "none") -> - deferred = $q.defer() - - queuedHttp + return queuedHttp .post("/project/new", { _csrf: window.csrfToken projectName: name @@ -273,13 +271,7 @@ define [ # to the rest of the app } $scope.updateVisibleProjects() - deferred.resolve(data.project_id) ) - .error((data, status, headers, config) -> - deferred.reject() - ) - - return deferred.promise $scope.openCreateProjectModal = (template = "none") -> event_tracking.send 'project-list-page-interaction', 'new-project', template @@ -294,15 +286,13 @@ define [ modalInstance.result.then (project_id) -> window.location = "/project/#{project_id}" - MAX_PROJECT_NAME_LENGTH = 150 $scope.renameProject = (project, newName) -> - if !newName? or newName.length == 0 or newName.length > MAX_PROJECT_NAME_LENGTH - return - project.name = newName - queuedHttp.post "/project/#{project.id}/rename", { - newProjectName: project.name + return queuedHttp.post "/project/#{project.id}/rename", { + newProjectName: newName, _csrf: window.csrfToken } + .success () -> + project.name = newName $scope.openRenameProjectModal = () -> project = $scope.getFirstSelectedProject() @@ -312,16 +302,11 @@ define [ templateUrl: "renameProjectModalTemplate" controller: "RenameProjectModalController" resolve: - projectName: () -> project.name - ) - - modalInstance.result.then( - (newName) -> - $scope.renameProject(project, newName) + project: () -> project + scope: $scope ) $scope.cloneProject = (project, cloneName) -> - deferred = $q.defer() event_tracking.send 'project-list-page-interaction', 'project action', 'Clone' queuedHttp .post("/project/#{project.id}/clone", { @@ -337,13 +322,7 @@ define [ # to the rest of the app } $scope.updateVisibleProjects() - deferred.resolve(data.project_id) ) - .error((data, status, headers, config) -> - deferred.reject() - ) - - return deferred.promise $scope.openCloneProjectModal = () -> project = $scope.getFirstSelectedProject() diff --git a/services/web/public/coffee/services/queued-http.coffee b/services/web/public/coffee/services/queued-http.coffee index c4c6862fae..8bf208f500 100644 --- a/services/web/public/coffee/services/queued-http.coffee +++ b/services/web/public/coffee/services/queued-http.coffee @@ -19,24 +19,29 @@ define [ processPendingRequests() queuedHttp = (args...) -> - deferred = $q.defer() - promise = deferred.promise + # We can't use Angular's $q.defer promises, because it only passes + # a single argument on error, and $http passes multiple. + promise = {} + successCallbacks = [] + errorCallbacks = [] # Adhere to the $http promise conventions promise.success = (callback) -> - promise.then(callback) + successCallbacks.push callback return promise promise.error = (callback) -> - promise.catch(callback) + errorCallbacks.push callback return promise doRequest = () -> $http(args...) - .success (successArgs...) -> - deferred.resolve(successArgs...) - .error (errorArgs...) -> - deferred.reject(errorArgs...) + .success (args...) -> + for cb in successCallbacks + cb(args...) + .error (args...) -> + for cb in errorCallbacks + cb(args...) pendingRequests.push doRequest processPendingRequests() diff --git a/services/web/public/js/libs/raven-3.15.0.min.js b/services/web/public/js/libs/raven-3.15.0.min.js new file mode 100644 index 0000000000..af5f59820a --- /dev/null +++ b/services/web/public/js/libs/raven-3.15.0.min.js @@ -0,0 +1,3 @@ +/*! Raven.js 3.15.0 (b7e98e5) | github.com/getsentry/raven-js */ +!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.Raven=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g ",i=h.length;a&&f++1&&g+e.length*i+b.length>=d));)e.push(b),g+=b.length,a=a.parentNode;return e.reverse().join(h)}function s(a){var b,c,d,e,f,g=[];if(!a||!a.tagName)return"";if(g.push(a.tagName.toLowerCase()),a.id&&g.push("#"+a.id),b=a.className,b&&h(b))for(c=b.split(/\s+/),f=0;fthis.k.maxBreadcrumbs&&this.t.shift(),this},addPlugin:function(a){var b=[].slice.call(arguments,1);return this.q.push([a,b]),this.m&&this.C(),this},setUserContext:function(a){return this.j.user=a,this},setExtraContext:function(a){return this.Q("extra",a),this},setTagsContext:function(a){return this.Q("tags",a),this},clearContext:function(){return this.j={},this},getContext:function(){return JSON.parse(y(this.j))},setEnvironment:function(a){return this.k.environment=a,this},setRelease:function(a){return this.k.release=a,this},setDataCallback:function(a){var b=this.k.dataCallback;return this.k.dataCallback=g(a)?function(c){return a(c,b)}:a,this},setBreadcrumbCallback:function(a){var b=this.k.breadcrumbCallback;return this.k.breadcrumbCallback=g(a)?function(c){return a(c,b)}:a,this},setShouldSendCallback:function(a){var b=this.k.shouldSendCallback;return this.k.shouldSendCallback=g(a)?function(c){return a(c,b)}:a,this},setTransport:function(a){return this.k.transport=a,this},lastException:function(){return this.d},lastEventId:function(){return this.f},isSetup:function(){return!!this.a&&(!!this.g||(this.ravenNotConfiguredError||(this.ravenNotConfiguredError=!0,this.y("error","Error: Raven has not been configured.")),!1))},afterLoad:function(){var a=G.RavenConfig;a&&this.config(a.dsn,a.config).install()},showReportDialog:function(a){if(H){a=a||{};var b=a.eventId||this.lastEventId();if(!b)throw new z("Missing eventId");var c=a.dsn||this.E;if(!c)throw new z("Missing DSN");var d=encodeURIComponent,e="";e+="?eventId="+d(b),e+="&dsn="+d(c);var f=a.user||this.j.user;f&&(f.name&&(e+="&name="+d(f.name)),f.email&&(e+="&email="+d(f.email)));var g=this.G(this.D(c)),h=H.createElement("script");h.async=!0,h.src=g+"/api/embed/error-page/"+e,(H.head||H.body).appendChild(h)}},I:function(){var a=this;this.l+=1,setTimeout(function(){a.l-=1})},R:function(a,b){var c,d;if(this.b){b=b||{},a="raven"+a.substr(0,1).toUpperCase()+a.substr(1),H.createEvent?(c=H.createEvent("HTMLEvents"),c.initEvent(a,!0,!0)):(c=H.createEventObject(),c.eventType=a);for(d in b)m(b,d)&&(c[d]=b[d]);if(H.createEvent)H.dispatchEvent(c);else try{H.fireEvent("on"+c.eventType.toLowerCase(),c)}catch(e){}}},S:function(a){var b=this;return function(c){if(b.T=null,b.u!==c){b.u=c;var d;try{d=r(c.target)}catch(e){d=""}b.captureBreadcrumb({category:"ui."+a,message:d})}}},U:function(){var a=this,b=1e3;return function(c){var d;try{d=c.target}catch(e){return}var f=d&&d.tagName;if(f&&("INPUT"===f||"TEXTAREA"===f||d.isContentEditable)){var g=a.T;g||a.S("input")(c),clearTimeout(g),a.T=setTimeout(function(){a.T=null},b)}}},V:function(a,b){var c=p(this.v.href),d=p(b),e=p(a);this.w=b,c.protocol===d.protocol&&c.host===d.host&&(b=d.relative),c.protocol===e.protocol&&c.host===e.host&&(a=e.relative),this.captureBreadcrumb({category:"navigation",data:{to:b,from:a}})},A:function(){function a(a){return function(b,d){for(var e=new Array(arguments.length),f=0;f2?arguments[2]:void 0;return c&&b.V(b.w,c+""),a.apply(this,arguments)}},d)}if(c.console&&"console"in G&&console.log){var m=function(a,c){b.captureBreadcrumb({message:a,level:c.level,category:"console"})};j(["debug","info","warn","error","log"],function(a,b){D(console,b,m)})}},M:function(){for(var a;this.s.length;){a=this.s.shift();var b=a[0],c=a[1],d=a[2];b[c]=d}},C:function(){var a=this;j(this.q,function(b,c){var d=c[0],e=c[1];d.apply(a,[a].concat(e))})},D:function(a){var b=F.exec(a),c={},d=7;try{for(;d--;)c[E[d]]=b[d]||""}catch(e){throw new z("Invalid DSN: "+a)}if(c.pass&&!this.k.allowSecretKey)throw new z("Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key");return c},G:function(a){var b="//"+a.host+(a.port?":"+a.port:"");return a.protocol&&(b=a.protocol+":"+b),b},z:function(){this.l||this.N.apply(this,arguments)},N:function(a,b){var c=this.O(a,b);this.R("handle",{stackInfo:a,options:b}),this.X(a.name,a.message,a.url,a.lineno,c,b)},O:function(a,b){var c=this,d=[];if(a.stack&&a.stack.length&&(j(a.stack,function(a,b){var e=c.Y(b);e&&d.push(e)}),b&&b.trimHeadFrames))for(var e=0;e0&&(a.breadcrumbs={values:[].slice.call(this.t,0)}),i(a.tags)&&delete a.tags,this.j.user&&(a.user=this.j.user),b.environment&&(a.environment=b.environment),b.release&&(a.release=b.release),b.serverName&&(a.server_name=b.serverName),g(b.dataCallback)&&(a=b.dataCallback(a)||a),a&&!i(a)&&(!g(b.shouldSendCallback)||b.shouldSendCallback(a)))return this.ca()?void this.y("warn","Raven dropped error due to backoff: ",a):void("number"==typeof b.sampleRate?Math.random()=0;--b)s[b]===a&&s.splice(b,1)}function c(){n(),s=[]}function k(a,b){var c=null;if(!b||f.collectWindowErrors){for(var d in s)if(s.hasOwnProperty(d))try{s[d].apply(null,[a].concat(h.call(arguments,2)))}catch(e){c=e}if(c)throw c}}function l(a,b,c,g,h){var l=null;if(v)f.computeStackTrace.augmentStackTraceWithInitialElement(v,b,c,a),o();else if(h&&e.isError(h))l=f.computeStackTrace(h),k(l,!0);else{var m,n={url:b,line:c,column:g},p=void 0,r=a;if("[object String]"==={}.toString.call(a)){var m=a.match(j);m&&(p=m[1],r=m[2])}n.func=i,l={name:p,message:r,url:d(),stack:[n]},k(l,!0)}return!!q&&q.apply(this,arguments)}function m(){r||(q=g.onerror,g.onerror=l,r=!0)}function n(){r&&(g.onerror=q,r=!1,q=void 0)}function o(){var a=v,b=t;t=null,v=null,u=null,k.apply(null,[a,!1].concat(b))}function p(a,b){var c=h.call(arguments,1);if(v){if(u===a)return;o()}var d=f.computeStackTrace(a);if(v=d,u=a,t=c,setTimeout(function(){u===a&&o()},d.incomplete?2e3:0),b!==!1)throw a}var q,r,s=[],t=null,u=null,v=null;return p.subscribe=a,p.unsubscribe=b,p.uninstall=c,p}(),f.computeStackTrace=function(){function a(a){if("undefined"!=typeof a.stack&&a.stack){for(var b,c,e,f=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,g=/^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i,h=/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,j=/(\S+) line (\d+)(?: > eval line \d+)* > eval/i,k=/\((\S*)(?::(\d+))(?::(\d+))\)/,l=a.stack.split("\n"),m=[],n=(/^(.*) is undefined$/.exec(a.message),0),o=l.length;n eval")>-1;q&&(b=j.exec(c[3]))?(c[3]=b[1],c[4]=b[2],c[5]=null):0!==n||c[5]||"undefined"==typeof a.columnNumber||(m[0].column=a.columnNumber+1),e={url:c[3],func:c[1]||i,args:c[2]?c[2].split(","):[],line:c[4]?+c[4]:null,column:c[5]?+c[5]:null}}!e.func&&e.line&&(e.func=i),m.push(e)}return m.length?{name:a.name,message:a.message,url:d(),stack:m}:null}}function b(a,b,c,d){var e={url:b,line:c};if(e.url&&e.line){if(a.incomplete=!1,e.func||(e.func=i),a.stack.length>0&&a.stack[0].url===e.url){if(a.stack[0].line===e.line)return!1;if(!a.stack[0].line&&a.stack[0].func===e.func)return a.stack[0].line=e.line,!1}return a.stack.unshift(e),a.partial=!0,!0}return a.incomplete=!0,!1}function c(a,g){for(var h,j,k=/function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,l=[],m={},n=!1,o=c.caller;o&&!n;o=o.caller)if(o!==e&&o!==f.report){if(j={url:null,func:i,line:null,column:null},o.name?j.func=o.name:(h=k.exec(o.toString()))&&(j.func=h[1]),"undefined"==typeof j.func)try{j.func=h.input.substring(0,h.input.indexOf("{"))}catch(p){}m[""+o]?n=!0:m[""+o]=!0,l.push(j)}g&&l.splice(0,g);var q={name:a.name,message:a.message,url:d(),stack:l};return b(q,a.sourceURL||a.fileName,a.line||a.lineNumber,a.message||a.description),q}function e(b,e){var g=null;e=null==e?0:+e;try{if(g=a(b))return g}catch(h){if(f.debug)throw h}try{if(g=c(b,e+1))return g}catch(h){if(f.debug)throw h}return{name:b.name,message:b.message,url:d()}}return e.augmentStackTraceWithInitialElement=b,e.computeStackTraceFromStackProp=a,e}(),b.exports=f}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{5:5}],7:[function(a,b,c){"use strict";function d(a,b){for(var c=0;c0){var h=d(c,this);~h?c.splice(h+1):c.push(this),~h?e.splice(h,1/0,f):e.push(f),~d(c,g)&&(g=b.call(this,f,g))}else c.push(g);return null==a?g:a.call(this,f,g)}}c=b.exports=e,c.getSerialize=f},{}]},{},[4])(4)}); +//# sourceMappingURL=raven.min.js.map \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index db51462620..3c3aae945a 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -35,11 +35,8 @@ } .global-alerts { - position: absolute; - z-index: 20; - top: 2px; - left: 0; - right: 0; + height: 0; + margin-top: 2px; text-align: center; .alert { @@ -49,6 +46,8 @@ padding: (@line-height-computed / 4); font-size: 14px; margin-bottom: (@line-height-computed / 4); + position: relative; + z-index: 20; } } #try-reconnect-now-button { diff --git a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee index 72285a1f44..0b7bfb1d2b 100644 --- a/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Editor/EditorControllerTests.coffee @@ -547,7 +547,7 @@ describe "EditorController", -> @err = "errro" @window_id = "kdsjklj290jlk" @newName = "new name here" - @ProjectDetailsHandler.renameProject = sinon.stub().callsArgWith(2, @err) + @ProjectDetailsHandler.renameProject = sinon.stub().callsArg(2) @EditorRealTimeController.emitToRoom = sinon.stub() it "should call the EditorController", (done)-> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee index 87120deced..626ff71442 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectControllerTests.coffee @@ -280,20 +280,12 @@ describe "ProjectController", -> done() @ProjectController.renameProject @req, @res - it "should send a 500 if there is a problem", (done)-> - @EditorController.renameProject.callsArgWith(2, "problem") - @res.sendStatus = (code)=> - code.should.equal 500 - @EditorController.renameProject.calledWith(@project_id, @newProjectName).should.equal true + it "should send an error to next() if there is a problem", (done)-> + @EditorController.renameProject.callsArgWith(2, error = new Error("problem")) + next = (e)=> + e.should.equal error done() - @ProjectController.renameProject @req, @res - - it "should return an error if the name is over 150 chars", (done)-> - @req.body.newProjectName = "EDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOTEDMUBEEBKBXUUUZERMNSXFFWIBHGSDAWGMRIQWJBXGWSBVWSIKLFPRBYSJEKMFHTRZBHVKJSRGKTBHMJRXPHORFHAKRNPZGGYIOT" - @res.sendStatus = (code)=> - code.should.equal 400 - done() - @ProjectController.renameProject @req, @res + @ProjectController.renameProject @req, @res, next describe "loadEditor", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee index f88d1700fd..8aa750b80a 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectCreationHandlerTests.coffee @@ -34,6 +34,8 @@ describe 'ProjectCreationHandler', -> addDoc: sinon.stub().callsArgWith(4, null, {_id: docId}) addFile: sinon.stub().callsArg(4) setRootDoc: sinon.stub().callsArg(2) + @ProjectDetailsHandler = + validateProjectName: sinon.stub().yields() @user = first_name:"first name here" @@ -48,6 +50,7 @@ describe 'ProjectCreationHandler', -> '../../models/Project':{Project:@ProjectModel} '../../models/Folder':{Folder:@FolderModel} './ProjectEntityHandler':@ProjectEntityHandler + "./ProjectDetailsHandler":@ProjectDetailsHandler "settings-sharelatex": @Settings = {} 'logger-sharelatex': {log:->} "metrics-sharelatex": { @@ -96,6 +99,18 @@ describe 'ProjectCreationHandler', -> it 'should return the error to the callback', -> should.exist @callback.args[0][0] + + describe "with an invalid name", -> + beforeEach -> + @ProjectDetailsHandler.validateProjectName = sinon.stub().yields(new Error("bad name")) + @handler.createBlankProject ownerId, projectName, @callback + + it 'should return the error to the callback', -> + should.exist @callback.args[0][0] + + it 'should not try to create the project', -> + @ProjectModel::save.called.should.equal false + describe 'Creating a basic project', -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee index 501e48f1a5..c4c3b3ef07 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectDetailsHandlerTests.coffee @@ -4,6 +4,7 @@ Errors = require "../../../../app/js/Features/Errors/Errors" SandboxedModule = require('sandboxed-module') sinon = require('sinon') assert = require("chai").assert +expect = require("chai").expect require('chai').should() describe 'ProjectDetailsHandler', -> @@ -95,6 +96,7 @@ describe 'ProjectDetailsHandler', -> describe "renameProject", -> beforeEach -> + @handler.validateProjectName = sinon.stub().yields() @ProjectModel.update.callsArgWith(2) @newName = "new name here" @@ -108,7 +110,34 @@ describe 'ProjectDetailsHandler', -> @handler.renameProject @project_id, @newName, => @tpdsUpdateSender.moveEntity.calledWith({project_id:@project_id, project_name:@project.name, newProjectName:@newName}).should.equal true done() + + it "should not do anything with an invalid name", (done) -> + @handler.validateProjectName = sinon.stub().yields(new Error("invalid name")) + @handler.renameProject @project_id, @newName, => + @tpdsUpdateSender.moveEntity.called.should.equal false + @ProjectModel.update.called.should.equal false + done() + describe "validateProjectName", -> + it "should reject empty names", (done) -> + @handler.validateProjectName "", (error) -> + expect(error).to.exist + done() + + it "should reject empty names with /s", (done) -> + @handler.validateProjectName "foo/bar", (error) -> + expect(error).to.exist + done() + + it "should reject long names", (done) -> + @handler.validateProjectName new Array(1000).join("a"), (error) -> + expect(error).to.exist + done() + + it "should accept normal names", (done) -> + @handler.validateProjectName "foobar", (error) -> + expect(error).to.not.exist + done() describe "setPublicAccessLevel", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee index f3dcda07cf..187d59d03d 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee @@ -80,6 +80,7 @@ describe 'ProjectEntityHandler', -> './ProjectUpdateHandler': @projectUpdater "./ProjectGetter": @ProjectGetter "settings-sharelatex":@settings + "../Cooldown/CooldownManager": @CooldownManager = {} describe 'mkdirp', -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee index 9257c2e83e..3d9808503b 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectLocatorTests.coffee @@ -70,7 +70,6 @@ describe 'ProjectLocator', -> it 'should give error if element could not be found', (done)-> @locator.findElement {project_id:project._id, element_id:"ddsd432nj42", type:"docs"}, (err, foundElement, path, parentFolder)-> - console.log err err.should.deep.equal new Errors.NotFoundError("entity not found") done() diff --git a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee index 5922a027e3..a68a9be1f1 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectUpdateHandlerTests.coffee @@ -11,6 +11,7 @@ describe 'ProjectUpdateHandler', -> @ProjectModel.update = sinon.stub().callsArg(3) @handler = SandboxedModule.require modulePath, requires: '../../models/Project':{Project:@ProjectModel} + 'logger-sharelatex' : { log: sinon.stub() } describe 'marking a project as recently updated', -> it 'should send an update to mongo', (done)-> diff --git a/services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee b/services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee index 73c178f934..dc3fc84dfa 100644 --- a/services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserLocatorTests.coffee @@ -11,6 +11,7 @@ describe "UserLocator", -> @UserLocator = SandboxedModule.require modulePath, requires: "../../infrastructure/mongojs": db: @db = { users: {} } "metrics-sharelatex": timeAsyncMethod: sinon.stub() + 'logger-sharelatex' : { log: sinon.stub() } @db.users = findOne : sinon.stub().callsArgWith(1, null, @user) diff --git a/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee index c59b481dc5..d0b96da2de 100644 --- a/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserRegistrationHandlerTests.coffee @@ -96,7 +96,6 @@ describe "UserRegistrationHandler", -> it "should return email registered in the error if there is a non holdingAccount there", (done)-> @User.findOne.callsArgWith(1, null, @user = {holdingAccount:false}) @handler.registerNewUser @passingRequest, (err, user)=> - console.log err, user err.should.deep.equal new Error("EmailAlreadyRegistered") user.should.deep.equal @user done()