diff --git a/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee b/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee index 4a0162eca0..2e8748281f 100644 --- a/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorUpdatesController.coffee @@ -18,9 +18,11 @@ module.exports = EditorUpdatesController = if takeSnapshot AutomaticSnapshotManager.markProjectAsUpdated(project_id) + logger.log doc_id: doc_id, project_id: project_id, client_id: update.meta?.source, version: update.v, "sending update to doc updater" + DocumentUpdaterHandler.queueChange project_id, doc_id, update, (error) -> if error? - logger.error err:error, project_id: project_id, "document was not available for update" + logger.error err:error, project_id: project_id, doc_id: doc_id, client_id: update.meta?.source, version: update.v, "document was not available for update" client.disconnect() callback(error) @@ -52,8 +54,10 @@ module.exports = EditorUpdatesController = io = require('../../infrastructure/Server').io for client in io.sockets.clients(doc_id) if client.id == update.meta.source + logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, "distributing update to sender" client.emit "otUpdateApplied", v: update.v, doc: update.doc else + logger.log doc_id: doc_id, version: update.v, source: update.meta?.source, client_id: client.id, "distributing update to collaborator" client.emit "otUpdateApplied", update _processErrorFromDocumentUpdater: (doc_id, error, message) -> diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee index 35b629de6a..b4b3954d31 100644 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ b/services/web/app/coffee/Features/Email/EmailSender.coffee @@ -2,37 +2,43 @@ logger = require('logger-sharelatex') metrics = require('../../infrastructure/Metrics') Settings = require('settings-sharelatex') metrics = require("../../infrastructure/Metrics") -ses = require('node-ses') +nodemailer = require("nodemailer") if Settings.email? and Settings.email.fromAddress? defaultFromAddress = Settings.email.fromAddress -else +else defaultFromAddress = "" -if Settings.email?.ses? and Settings.email.ses?.key? and Settings.email.ses?.key != "" and Settings.email.ses?.secret? and Settings.email.ses?.secret != "" - client = ses.createClient({ key: Settings.email.ses.key, secret: Settings.email.ses.secret }); -else - logger.warn "AWS SES credentials are not configured. No emails will be sent." - client = - sendemail: (options, callback = (err, data, res) ->) -> - logger.log options: options, "would send email if SES credentials enabled" - callback() +# provide dummy mailer unless we have a better one configured. +client = + sendMail: (options, callback = (err,status) ->) -> + 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." module.exports = - sendEmail : (options, callback = (error) ->)-> logger.log receiver:options.to, subject:options.subject, "sending email" metrics.inc "email" - options = + options = to: options.to from: defaultFromAddress subject: options.subject - message: options.html + html: options.html replyTo: options.replyTo || Settings.email.replyToAddress - client.sendemail options, (err, data, res)-> + client.sendMail options, (err, res)-> if err? logger.err err:err, "error sending message" else logger.log "Message sent to #{options.to}" callback(err) - \ No newline at end of file + diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 764ae8afd6..a4f0837bc1 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -217,6 +217,10 @@ module.exports = class Router require("./models/Project").Project.findOne {}, () -> throw new Error("Test error") + app.post '/error/client', (req, res, next) -> + logger.error err: req.body.error, meta: req.body.meta, "client side error" + res.send(204) + app.get '*', HomeController.notFound diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 50d595cd89..396d8a4cb1 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -22,6 +22,9 @@ html(itemscope, itemtype='http://schema.org/Product') })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); ga('create', '#{gaToken}', 'sharelatex.com'); ga('send', 'pageview'); + - else + script(type='text/javascript') + window.ga = function() {}; script window.csrfToken = "#{csrfToken}"; diff --git a/services/web/app/views/modals.jade b/services/web/app/views/modals.jade index ffd110760f..ef85ce7da9 100644 --- a/services/web/app/views/modals.jade +++ b/services/web/app/views/modals.jade @@ -77,15 +77,6 @@ .modal-body span.message -#genericServerErrorModal(style='display: none') - .modal - .modal-header - h3 There was a problem talking to the server - .modal-body - span.message Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know. - .modal-footer - button.btn.btn-primary.cancel Ok - #projectUploadModal(style='display: none') .modal .modal-header diff --git a/services/web/app/views/templates.jade b/services/web/app/views/templates.jade index 586719a62d..4ab4640e1f 100644 --- a/services/web/app/views/templates.jade +++ b/services/web/app/views/templates.jade @@ -136,16 +136,6 @@ #subscribeForm .modal-footer - - script(type="text/template")#genericModalTemplateWithButton - .modal - .modal-header - h3 {{ title }} - .modal-body - .message {{ message }} - .modal-footer - button.btn.btn-primary ok - script(type="text/template")#genericModalButtonTemplate button(class="btn {{ class }}") {{ text }} @@ -300,22 +290,29 @@ -if(session && session.user && session.user.isAdmin) .box .page-header - h2 Publish Project + h2 Publish project as template + + #publishedAsTemplateArea.show-when-published.alert.alert-success + p + .btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish + i.icon-ok + | Your project is currently published. + a#templateLink(href='{{canonicalUrl}}') View in template gallery. + p + | Lastest version: {{publishedDate}}. - #publishedAsTemplateArea(style="display:none;") - a#templateLink(href='{{canonicalUrl}}') View Template - p published at {{publishedDate}} - .btn.btn-warning#unPublishProjectAsTemplate unpublish project as template - .btn.btn-success#republishProjectAsTemplate re publish project as template - - #problemWithPublishingArea(style="display:none;") + #problemWithPublishingArea p There is a problem with our publishing service, please try again in a few minutes. - #publishWorkingArea(style="display:none;") - p Working..... - #unpublishedAsTemplateArea(style="display:none;") - .btn.btn-success#publishProjectAsTemplate Publish project as template - div - textarea.span6#projectDescription {{description}} + #publishWorkingArea + p Working... + div.show-when-published.show-when-unpublished.project-description + label(for="project-description") Description + .row-fluid + textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}} + #unpublishedAsTemplateArea.show-when-unpublished + .btn.btn-success#publishProjectAsTemplate Publish + p.show-when-published + button.btn.btn-success#republishProjectAsTemplate Re-Publish script(type="text/template")#settingsPanelTemplate diff --git a/services/web/config/settings.development.coffee b/services/web/config/settings.development.coffee index eefda7b1a5..8b6344fbce 100644 --- a/services/web/config/settings.development.coffee +++ b/services/web/config/settings.development.coffee @@ -134,6 +134,22 @@ module.exports = {name: "English", code: "en"} ] + # Email support + # ------------- + # + # ShareLaTeX uses nodemailer (http://www.nodemailer.com/) to send transactional emails. + # To see the range of transport and options they support, see http://www.nodemailer.com/docs/transports + #email: + # fromAddress: "" + # replyTo: "" + # lifecycle: false + ## Example transport and parameter settings for Amazon SES + # transport: "SES" + # parameters: + # AWSAccessKeyID: "" + # AWSSecretKey: "" + + # Third party services # -------------------- # @@ -153,15 +169,6 @@ module.exports = # tenderUrl: "" # - # email: - # fromAddress: "" - # replyTo: "" - # lifecycle: false - # ShareLaTeX uses Amazon's SES api to send transactional emails. - # Uncomment these lines and provide your credentials to be able to send emails. - # ses: - # "key":"" - # "secret":"" # Production Settings # ------------------- diff --git a/services/web/package.json b/services/web/package.json index 46830b5875..4d08ecc1f2 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -32,7 +32,7 @@ "fairy": "0.0.2", "node-uuid": "1.4.0", "mongojs": "0.9.8", - "node-ses": "0.0.3", + "nodemailer": "0.6.1", "bcrypt": "0.7.5", "archiver": "0.5.1", "nodetime": "0.8.15", diff --git a/services/web/public/coffee/auto-complete/Snippets.coffee b/services/web/public/coffee/auto-complete/Snippets.coffee index 06332562c9..6856acbac0 100644 --- a/services/web/public/coffee/auto-complete/Snippets.coffee +++ b/services/web/public/coffee/auto-complete/Snippets.coffee @@ -79,6 +79,14 @@ define () -> \\end{enumerate} """ meta: "env" + }, { + caption: "\\begin{itemize}..." + snippet: """ + \\begin{itemize} + \\item $1 + \\end{itemize} + """ + meta: "env" }, { caption: "\\begin{frame}..." snippet: """ diff --git a/services/web/public/coffee/editor/Document.coffee b/services/web/public/coffee/editor/Document.coffee index 41cf1f7e11..d08d4baca4 100644 --- a/services/web/public/coffee/editor/Document.coffee +++ b/services/web/public/coffee/editor/Document.coffee @@ -136,16 +136,18 @@ define [ @unBindFromSocketEvents() _bindToShareJsDocEvents: () -> - @doc.on "error", (error) => @_onError error + @doc.on "error", (error, meta) => @_onError error, meta @doc.on "externalUpdate", () => @trigger "externalUpdate" @doc.on "remoteop", () => @trigger "remoteop" @doc.on "op:sent", () => @trigger "op:sent" @doc.on "op:acknowledged", () => @trigger "op:acknowledged" - _onError: (error) -> - console.error "ShareJS error", error - ga('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" ) + _onError: (error, meta = {}) -> + console.error "ShareJS error", error, meta + ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{ide.socket.socket.transport.name}" ) @ide.socket.disconnect() + meta.doc_id = @doc_id + @ide.reportError(error, meta) @doc?.clearInflightAndPendingOps() @_cleanUp() @trigger "error", error diff --git a/services/web/public/coffee/editor/Editor.coffee b/services/web/public/coffee/editor/Editor.coffee index d4313fe6eb..c7eb19d9d8 100644 --- a/services/web/public/coffee/editor/Editor.coffee +++ b/services/web/public/coffee/editor/Editor.coffee @@ -121,7 +121,7 @@ define [ @aceEditor = aceEditor = AceEditor.edit("editor") @on "resize", => @aceEditor.resize() - @ide.layoutManager.on "resize", => @aceEditor.resize() + @ide.layoutManager.on "resize", => @trigger "resize" mode = window.userSettings.mode theme = window.userSettings.theme diff --git a/services/web/public/coffee/editor/ShareJsDoc.coffee b/services/web/public/coffee/editor/ShareJsDoc.coffee index e887ddf588..78d3232994 100644 --- a/services/web/public/coffee/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/editor/ShareJsDoc.coffee @@ -94,14 +94,17 @@ define [ INFLIGHT_OP_TIMEOUT: 10000 _startInflightOpTimeout: (update) -> + meta = + v: update.v + op_sent_at: new Date() timer = setTimeout () => - @_handleError "Doc op was not acknowledged in time" + @_handleError new Error("Doc op was not acknowledged in time"), meta , @INFLIGHT_OP_TIMEOUT @_doc.inflightCallbacks.push () => clearTimeout timer - _handleError: (error) -> - @trigger "error", error + _handleError: (error, meta = {}) -> + @trigger "error", error, meta _bindToDocChanges: (doc) -> submitOp = doc.submitOp diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index e98163f87d..5b6eb591a7 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -48,7 +48,7 @@ define [ SearchManager, Project, User, - StandaloneModal, + Modal, FileTreeManager, MessageManager, HelpManager, @@ -144,16 +144,39 @@ define [ setTimeout(joinProject, 100) showErrorModal: (title, message)-> - modalOptions = - templateId:'genericModalTemplate' - isStatic: false + new Modal { title: title - message:message - new Modal modalOptions + message: message + buttons: [ text: "OK" ] + } - showGenericServerErrorMessage: (message)-> - new Modal - templateId : "genericServerErrorModal" + showGenericServerErrorMessage: ()-> + new Modal { + title: "There was a problem talking to the server" + message: "Sorry, we couldn't complete your request right now. Please wait a few moments and try again. If the problem persists, please let us know." + buttons: [ text: "OK" ] + } + + reportError: (error, meta = {}) -> + meta.client_id = @socket?.socket?.sessionid + meta.transport = @socket?.socket?.transport?.name + meta.client_now = new Date() + meta.last_connected = @connectionManager.lastConnected + meta.second_last_connected = @connectionManager.secondLastConnected + meta.last_disconnected = @connectionManager.lastDisconnected + meta.second_last_disconnected = @connectionManager.secondLastDisconnected + errorObj = {} + for key in Object.getOwnPropertyNames(error) + errorObj[key] = error[key] + $.ajax + url: "/error/client" + type: "POST" + data: JSON.stringify + error: errorObj + meta: meta + contentType: "application/json; charset=utf-8" + headers: + "X-Csrf-Token": window.csrfToken setLoadingMessage: (message) -> $("#loadingMessage").text(message) @@ -171,45 +194,6 @@ define [ ide.layoutManager.resizeAllSplitters() ide.tourManager = new IdeTour ide - class Modal - #templateId, title, message, isStatic, cancelCallback - constructor: (options, completeCallback = () -> {})-> - html = $("##{options.templateId}").html() - modal = "
" - $('body').append(modal) - $modal = $('#modal') - - if options.title? - $modal.find('h3').text(options.title) - if options.message? - $modal.find('.message').text(options.message) - if options.inputValue? - $modal.find('input').val(options.inputValue) - - backdrop = true - if options.backdrop? - backdrop = options.backdrop - - $modal.modal backdrop:backdrop, show:true, keyboard:true, isStatic:options.isStatic - $modal.find('input').focus() - - $modal.find('button').click (e)=> - e.preventDefault() - $modal.modal('hide') - if e.target.className.indexOf("cancel") == -1 - inputval = $modal.find('input').val() - completeCallback(inputval) - - $modal.find('input').keydown (event)=> - code = event.keyCode || event.which - if code == 13 - $modal.find('button.primary').click() - - $modal.bind 'hide', ()-> - if options.cancelCallback? - options.cancelCallback() - $('#modal').remove() - ide.savingAreaManager = $savingArea : $('#saving-area') timeOut: undefined @@ -220,7 +204,7 @@ define [ return if @timeOut? @clearTimeout() @timeOut = setTimeout((=> - ga('send', 'event', 'editor-interaction', 'notification-shown', "saving") + ga?('send', 'event', 'editor-interaction', 'notification-shown', "saving") $("#savingProblems").show() ), 1000) diff --git a/services/web/public/coffee/ide/ConnectionManager.coffee b/services/web/public/coffee/ide/ConnectionManager.coffee index 9556ec1d1c..18be9e007e 100644 --- a/services/web/public/coffee/ide/ConnectionManager.coffee +++ b/services/web/public/coffee/ide/ConnectionManager.coffee @@ -8,11 +8,15 @@ define [ @socket = @ide.socket @socket.on "connect", () => @connected = true + @secondLastConnected = @lastConnected + @lastConnected = new Date() @hideModal() @cancelReconnect() @socket.on 'disconnect', () => @connected = false + @secondLastDisconnected = @lastDisconnected + @lastDisconnected = new Date() @ide.trigger "disconnect" setTimeout(=> ga('send', 'event', 'editor-interaction', 'disconnect') diff --git a/services/web/public/coffee/project-members/ProjectMembersManager.coffee b/services/web/public/coffee/project-members/ProjectMembersManager.coffee index f68949f613..cb92104a0e 100644 --- a/services/web/public/coffee/project-members/ProjectMembersManager.coffee +++ b/services/web/public/coffee/project-members/ProjectMembersManager.coffee @@ -3,9 +3,10 @@ define [ "models/ProjectMemberList" "account/AccountManager" "utils/Modal" + "moment" "libs/backbone" "libs/mustache" -], (User, ProjectMemberList, AccountManager, Modal) -> +], (User, ProjectMemberList, AccountManager, Modal, moment) -> INFINITE_COLLABORATORS = -1 class ProjectMembersManager @@ -20,6 +21,8 @@ define [ name: "Share" content : $(@templates.userPanel) lock: true + onShown: () => + @publishProjectView?.refreshPublishStatus() setupPublish = _.once => @publishProjectView = new PublishProjectView @@ -177,25 +180,32 @@ define [ initialize: () -> @ide = @options.ide @model = @ide.project - _.bindAll(this, "render"); - this.model.bind('change', this.render) - @refreshPublishStatus() - + @render() render: -> viewModel = description: @model.get("description") canonicalUrl: @model.get("template.canonicalUrl") isPublished: @model.get("template.isPublished") - publishedDate: @model.get("template.publishedDate") + publishedDate: moment(@model.get("template.publishedDate")).format("Do MMM YYYY, h:mm a") - $(@el).html $(Mustache.to_html(@template, viewModel)) - @publishedArea = $('#publishedAsTemplateArea') - @unpublishedArea = $('#unpublishedAsTemplateArea') + @$el.html $(Mustache.to_html(@template, viewModel)) + @publishedArea = $('.show-when-published') + @unpublishedArea = $('.show-when-unpublished') + $('#problemWithPublishingArea').hide() + $('#publishWorkingArea').hide() + refreshView: () -> + if @model.get("template.isPublished") + @$("a#templateLink").attr("href", @model.get("template.canonicalUrl")) + @publishedArea.show() + else + @unpublishedArea.show() refreshPublishStatus: -> + @showWorking() @ide.socket.emit "getPublishedDetails", @ide.user.get("id"), (err, details)=> + @hideWorking() if err? return @showError() @@ -203,16 +213,17 @@ define [ if details.exists @model.set("template.canonicalUrl", details.canonicalUrl) @model.set("template.publishedDate", details.publishedDate) - @publishedArea.show() - @unpublishedArea.hide() - else - @publishedArea.hide() - @unpublishedArea.show() + + @refreshView() showError: -> + @publishedArea.hide() + @unpublishedArea.hide() $('#problemWithPublishingArea').show() showWorking: -> + @publishedArea.hide() + @unpublishedArea.hide() $('#publishWorkingArea').show() hideWorking: -> diff --git a/services/web/public/coffee/track-changes/DiffView.coffee b/services/web/public/coffee/track-changes/DiffView.coffee index 73006e2f3d..7fc95e77ba 100644 --- a/services/web/public/coffee/track-changes/DiffView.coffee +++ b/services/web/public/coffee/track-changes/DiffView.coffee @@ -16,11 +16,11 @@ define [ @createAceEditor() @aceEditor.setValue(@getPlainDiffContent()) @aceEditor.clearSelection() - session = @aceEditor.getSession() - session.setMode(new LatexMode.Mode()) - session.setUseWrapMode(true) + @$ace = $(@aceEditor.renderer.container).find(".ace_scroller") @insertMarkers() @insertNameTag() + @insertMoreChangeLabels() + @scrollToFirstChange() return @ destroy: () -> @@ -34,12 +34,18 @@ define [ @aceEditor.setTheme("ace/theme/#{window.userSettings.theme}") @aceEditor.setReadOnly true @aceEditor.setShowPrintMargin(false) + session = @aceEditor.getSession() + session.setMode(new LatexMode.Mode()) + session.setUseWrapMode(true) @aceEditor.on "mousemove", (e) => position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY) e.position = position @updateVisibleNames(e) + session.on "changeScrollTop", (e) => + @updateMoreChangeLabels() + getPlainDiffContent: () -> content = "" for entry in @model.get("diff") or [] @@ -102,7 +108,6 @@ define [ , foreground insertNameTag: () -> - @$ace = $(@aceEditor.renderer.container).find(".ace_scroller") @$nameTagEl = $("") @$nameTagEl.css({ position: "absolute" @@ -110,6 +115,19 @@ define [ @$nameTagEl.hide() @$ace.append(@$nameTagEl) + insertMoreChangeLabels: () -> + @$changesBefore = $("