diff --git a/services/web/.gitignore b/services/web/.gitignore index 96ebb86806..d7b110e28d 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -48,6 +48,7 @@ BackgroundJobsWorker.js UserAndProjectPopulator.coffee public/js/*.js +public/js/*.map public/js/libs/sharejs.js public/js/analytics/ public/js/directives/ diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index f2ce93f671..45a6528cdd 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -107,7 +107,9 @@ module.exports = (grunt) -> cwd: 'public/coffee', src: ['**/*.coffee'], dest: 'public/js/', - ext: '.js' + ext: '.js', + options: + sourceMap: true smoke_tests: expand: true, @@ -164,6 +166,7 @@ module.exports = (grunt) -> baseUrl: "./" dir: "public/minjs" inlineText: false + generateSourceMaps: true preserveLicenseComments: false paths: "moment": "libs/#{PackageVersions.lib('moment')}" diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index b661455028..49bd994b2c 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -29,8 +29,12 @@ module.exports = AuthenticationManager = callback null, null setUserPassword: (user_id, password, callback = (error) ->) -> - if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length + if (Settings.passwordStrengthOptions?.length?.max? and + Settings.passwordStrengthOptions?.length?.max < password.length) return callback("password is too long") + if (Settings.passwordStrengthOptions?.length?.min? and + Settings.passwordStrengthOptions?.length?.min > password.length) + return callback("password is too short") bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index 55006b73c7..c4f3089fe0 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -38,5 +38,5 @@ module.exports = ErrorController = 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" + logger.error err: error, url:req.url, method:req.method, "error passed to top level next middlewear" res.sendStatus(500) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 92750a5912..193965c101 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -275,6 +275,7 @@ module.exports = ProjectController = theme : user.ace.theme fontSize : user.ace.fontSize autoComplete: user.ace.autoComplete + autoPairDelimiters: user.ace.autoPairDelimiters pdfViewer : user.ace.pdfViewer syntaxValidation: user.ace.syntaxValidation } diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 05737061fe..6b8bd99c35 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -54,8 +54,8 @@ module.exports = renderSubscriptionGroupAdminPage: (req, res)-> user_id = AuthenticationController.getLoggedInUserId(req) SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> - if !subscription.groupPlan - return res.redirect("/") + if !subscription?.groupPlan + return res.redirect("/user/subscription") SubscriptionGroupHandler.getPopulatedListOfMembers user_id, (err, users)-> res.render "subscriptions/group_admin", title: 'group_admin' diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index c5d54a9c49..9a3e34ce5a 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -4,7 +4,7 @@ SubscriptionGroupController = require './SubscriptionGroupController' Settings = require "settings-sharelatex" module.exports = - apply: (webRouter, apiRouter) -> + apply: (webRouter, privateApiRouter, publicApiRouter) -> return unless Settings.enableSubscriptions webRouter.get '/user/subscription/plans', SubscriptionController.plansPage @@ -35,7 +35,7 @@ module.exports = webRouter.get '/user/subscription/:subscription_id/group/successful-join', AuthenticationController.requireLogin(), SubscriptionGroupController.renderSuccessfulJoinPage #recurly callback - apiRouter.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback + publicApiRouter.post '/user/subscription/callback', SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback #user changes their account state webRouter.post '/user/subscription/create', AuthenticationController.requireLogin(), SubscriptionController.createSubscription diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 7b857c6460..12faf0e234 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -62,6 +62,15 @@ module.exports = SubscriptionUpdater = invited_emails: email }, callback + deleteSubscription: (subscription_id, callback = (error) ->) -> + SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> + return callback(err) if err? + affected_user_ids = [subscription.admin_id].concat(subscription.member_ids or []) + logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users" + Subscription.remove {_id: ObjectId(subscription_id)}, (err) -> + return callback(err) if err? + async.mapSeries affected_user_ids, SubscriptionUpdater._setUsersMinimumFeatures, 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/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 65000985e1..9954294577 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -74,6 +74,8 @@ module.exports = UserController = user.ace.fontSize = req.body.fontSize if req.body.autoComplete? user.ace.autoComplete = req.body.autoComplete + if req.body.autoPairDelimiters? + user.ace.autoPairDelimiters = req.body.autoPairDelimiters if req.body.spellCheckLanguage? user.ace.spellCheckLanguage = req.body.spellCheckLanguage if req.body.pdfViewer? diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 100fd28bd6..498127cdbd 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -11,6 +11,7 @@ async = require("async") Modules = require "./Modules" Url = require "url" PackageVersions = require "./PackageVersions" +htmlEncoder = new require("node-html-encoder").Encoder("numerical") fingerprints = {} Path = require 'path' @@ -66,7 +67,7 @@ logger.log "Finished generating file fingerprints" cdnAvailable = Settings.cdn?.web?.host? darkCdnAvailable = Settings.cdn?.web?.darkHost? -module.exports = (app, webRouter, apiRouter)-> +module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> webRouter.use (req, res, next)-> res.locals.session = req.session next() @@ -82,7 +83,8 @@ module.exports = (app, webRouter, apiRouter)-> ) next() webRouter.use addSetContentDisposition - apiRouter.use addSetContentDisposition + privateApiRouter.use addSetContentDisposition + publicApiRouter.use addSetContentDisposition webRouter.use (req, res, next)-> req.externalAuthenticationSystemUsed = res.locals.externalAuthenticationSystemUsed = -> @@ -150,9 +152,10 @@ module.exports = (app, webRouter, apiRouter)-> next() webRouter.use (req, res, next)-> - res.locals.translate = (key, vars = {}) -> + res.locals.translate = (key, vars = {}, htmlEncode = false) -> vars.appName = Settings.appName - req.i18n.translate(key, vars) + str = req.i18n.translate(key, vars) + if htmlEncode then htmlEncoder.htmlEncode(str) else str # Don't include the query string parameters, otherwise Google # treats ?nocdn=true as the canonical version res.locals.currentUrl = Url.parse(req.originalUrl).pathname diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee index e7d521fa56..9d4da24c8b 100644 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ b/services/web/app/coffee/infrastructure/Modules.coffee @@ -15,13 +15,14 @@ module.exports = Modules = @modules.push loadedModule Modules.attachHooks() - applyRouter: (webRouter, apiRouter) -> + applyRouter: (webRouter, privateApiRouter, publicApiRouter) -> for module in @modules - module.router?.apply(webRouter, apiRouter) + module.router?.apply?(webRouter, privateApiRouter, publicApiRouter) - applyNonCsrfRouter: (webRouter, apiRouter) -> + applyNonCsrfRouter: (webRouter, privateApiRouter, publicApiRouter) -> for module in @modules - module.nonCsrfRouter?.apply(webRouter, apiRouter) + module.nonCsrfRouter?.apply(webRouter, privateApiRouter, publicApiRouter) + module.router?.applyNonCsrfRouter?(webRouter, privateApiRouter, publicApiRouter) viewIncludes: {} loadViewIncludes: (app) -> diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 4c031a8bd1..48f7fd3e65 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -52,7 +52,8 @@ else app = express() webRouter = express.Router() -apiRouter = express.Router() +privateApiRouter = express.Router() +publicApiRouter = express.Router() if Settings.behindProxy app.enable('trust proxy') @@ -108,7 +109,7 @@ Modules.hooks.fire 'passportSetup', passport, (err) -> if err? logger.err {err}, "error setting up passport in modules" -Modules.applyNonCsrfRouter(webRouter, apiRouter) +Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) webRouter.use csrfProtection webRouter.use translations.expressMiddlewear @@ -122,7 +123,7 @@ webRouter.use (req, res, next) -> next() webRouter.use ReferalConnect.use -expressLocals(app, webRouter, apiRouter) +expressLocals(app, webRouter, privateApiRouter, publicApiRouter) if app.get('env') == 'production' logger.info "Production Enviroment" @@ -142,11 +143,8 @@ webRouter.use (req, res, next) -> res.status(503) res.render("general/closed", {title:"maintenance"}) -apiRouter.get "/status", (req, res)-> - res.send("web sharelatex is alive") - profiler = require "v8-profiler" -apiRouter.get "/profile", (req, res) -> +privateApiRouter.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") profiler.startProfiling("test") setTimeout () -> @@ -161,15 +159,25 @@ app.get "/heapdump", (req, res)-> logger.info ("creating HTTP server").yellow 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) +# provide settings for separate web and api processes +# if enableApiRouter and enableWebRouter are not defined they default +# to true. +notDefined = (x) -> !x? +enableApiRouter = Settings.web?.enableApiRouter +if enableApiRouter or notDefined(enableApiRouter) + logger.info("providing api router"); + app.use(privateApiRouter) + app.use(ErrorController.handleApiError) -router = new Router(webRouter, apiRouter) +enableWebRouter = Settings.web?.enableWebRouter +if enableWebRouter or notDefined(enableWebRouter) + logger.info("providing web router"); + app.use(publicApiRouter) # public API goes with web router for public access + app.use(ErrorController.handleApiError) + app.use(webRouter) + app.use(ErrorController.handleError) +router = new Router(webRouter, privateApiRouter, publicApiRouter) module.exports = app: app diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index 099b9ef8e2..fce4b18bc0 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -24,6 +24,7 @@ UserSchema = new Schema theme : {type : String, default: 'textmate'} fontSize : {type : Number, default:'12'} autoComplete: {type : Boolean, default: true} + autoPairDelimiters: {type : Boolean, default: true} spellCheckLanguage : {type : String, default: "en"} pdfViewer : {type : String, default: "pdfjs"} syntaxValidation : {type : Boolean} diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 3679b29a40..2ebde53748 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -49,7 +49,7 @@ logger = require("logger-sharelatex") _ = require("underscore") module.exports = class Router - constructor: (webRouter, apiRouter)-> + constructor: (webRouter, privateApiRouter, publicApiRouter)-> if !Settings.allowPublicAccess webRouter.all '*', AuthenticationController.requireGlobalLogin @@ -67,17 +67,17 @@ module.exports = class Router AuthenticationController.addEndpointToLoginWhitelist '/register' - EditorRouter.apply(webRouter, apiRouter) - CollaboratorsRouter.apply(webRouter, apiRouter) - SubscriptionRouter.apply(webRouter, apiRouter) - UploadsRouter.apply(webRouter, apiRouter) - PasswordResetRouter.apply(webRouter, apiRouter) - StaticPagesRouter.apply(webRouter, apiRouter) - RealTimeProxyRouter.apply(webRouter, apiRouter) - ContactRouter.apply(webRouter, apiRouter) - AnalyticsRouter.apply(webRouter, apiRouter) + EditorRouter.apply(webRouter, privateApiRouter) + CollaboratorsRouter.apply(webRouter, privateApiRouter) + SubscriptionRouter.apply(webRouter, privateApiRouter, publicApiRouter) + UploadsRouter.apply(webRouter, privateApiRouter) + PasswordResetRouter.apply(webRouter, privateApiRouter) + StaticPagesRouter.apply(webRouter, privateApiRouter) + RealTimeProxyRouter.apply(webRouter, privateApiRouter) + ContactRouter.apply(webRouter, privateApiRouter) + AnalyticsRouter.apply(webRouter, privateApiRouter) - Modules.applyRouter(webRouter, apiRouter) + Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) if Settings.enableSubscriptions @@ -94,7 +94,14 @@ module.exports = class Router SudoModeMiddlewear.protectPage, UserPagesController.settingsPage webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings - webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword + webRouter.post '/user/password/update', + AuthenticationController.requireLogin(), + RateLimiterMiddlewear.rateLimit({ + endpointName: "change-password" + maxRequests: 10 + timeInterval: 60 + }), + UserController.changePassword webRouter.get '/user/sessions', AuthenticationController.requireLogin(), @@ -106,7 +113,7 @@ module.exports = class Router webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo - apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo + privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject @@ -211,15 +218,15 @@ module.exports = class Router # Deprecated in favour of /internal/project/:project_id but still used by versioning - apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails + privateApiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails # New 'stable' /internal API end points - apiRouter.get '/internal/project/:project_id', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails - apiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject - apiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf + privateApiRouter.get '/internal/project/:project_id', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails + privateApiRouter.get '/internal/project/:Project_id/zip', AuthenticationController.httpAuth, ProjectDownloadsController.downloadProject + privateApiRouter.get '/internal/project/:project_id/compile/pdf', AuthenticationController.httpAuth, CompileController.compileAndDownloadPdf - apiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects - apiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject + privateApiRouter.post '/internal/deactivateOldProjects', AuthenticationController.httpAuth, InactiveProjectController.deactivateOldProjects + privateApiRouter.post '/internal/project/:project_id/deactivate', AuthenticationController.httpAuth, InactiveProjectController.deactivateProject webRouter.get /^\/internal\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> @@ -230,14 +237,14 @@ module.exports = class Router next() ), AuthenticationController.httpAuth, CompileController.getFileFromClsi - apiRouter.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument - apiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument + privateApiRouter.get '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.getDocument + privateApiRouter.post '/project/:Project_id/doc/:doc_id', AuthenticationController.httpAuth, DocumentController.setDocument - apiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate - apiRouter.delete '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.deleteUpdate + privateApiRouter.post '/user/:user_id/update/*', AuthenticationController.httpAuth, TpdsController.mergeUpdate + privateApiRouter.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 + privateApiRouter.post '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.updateProjectContents + privateApiRouter.delete '/project/:project_id/contents/*', AuthenticationController.httpAuth, TpdsController.deleteProjectContents webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi @@ -268,19 +275,24 @@ module.exports = class Router webRouter.post '/admin/messages', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.createMessage webRouter.post '/admin/messages/clear', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.clearMessages - apiRouter.get '/perfTest', (req,res)-> + privateApiRouter.get '/perfTest', (req,res)-> res.send("hello") - apiRouter.get '/status', (req,res)-> - res.send("websharelatex is up") + publicApiRouter.get '/status', (req,res)-> + res.send("web sharelatex is alive (web)") + privateApiRouter.get '/status', (req,res)-> + res.send("web sharelatex is alive (api)") webRouter.get '/dev/csrf', (req, res) -> res.send res.locals.csrfToken - apiRouter.get '/health_check', HealthCheckController.check - apiRouter.get '/health_check/redis', HealthCheckController.checkRedis + publicApiRouter.get '/health_check', HealthCheckController.check + privateApiRouter.get '/health_check', HealthCheckController.check - apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> + publicApiRouter.get '/health_check/redis', HealthCheckController.checkRedis + privateApiRouter.get '/health_check/redis', HealthCheckController.checkRedis + + webRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> project_id = req.params.Project_id sendRes = _.once (statusCode, message)-> res.status statusCode @@ -303,7 +315,7 @@ module.exports = class Router else sendRes 500, "Compiler returned failure #{status}" - apiRouter.get "/ip", (req, res, next) -> + webRouter.get "/ip", (req, res, next) -> res.send({ ip: req.ip ips: req.ips @@ -316,7 +328,7 @@ module.exports = class Router require("./models/Project").Project.findOne {}, () -> throw new Error("Test error") - apiRouter.get '/opps-small', (req, res, next)-> + privateApiRouter.get '/opps-small', (req, res, next)-> logger.err "test error occured" res.send() diff --git a/services/web/app/views/contact-us-modal.pug b/services/web/app/views/contact-us-modal.pug index d3e5aa0e87..aad68a53d3 100644 --- a/services/web/app/views/contact-us-modal.pug +++ b/services/web/app/views/contact-us-modal.pug @@ -7,36 +7,61 @@ script(type='text/ng-template', id='supportModalTemplate') ) × h3 #{translate("contact_us")} .modal-body.contact-us-modal - span(ng-show="sent == false") - label - | #{translate("subject")} - .form-group - input.field.text.medium.span8.form-control( - ng-model="form.subject", - ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }" - maxlength='255', - tabindex='1', - onkeyup='') - .contact-suggestions(ng-show="suggestions.length") - p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "__kb__", kb: translate("knowledge_base") })} - ul.contact-suggestion-list - li(ng-repeat="suggestion in suggestions") - a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") - span(ng-bind-html="suggestion.name") - i.fa.fa-angle-right - label.desc(ng-show="'"+getUserEmail()+"'.length < 1") - | #{translate("email")} - .form-group(ng-show="'"+getUserEmail()+"'.length < 1") - input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') - label#title12.desc - | #{translate("project_url")} (#{translate("optional")}) - .form-group - input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') - label.desc - | #{translate("contact_message_label")} - .form-group - textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='') - .form-group.text-center - input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us")) + form(name="contactForm") + span(ng-show="sent == false") + .alert.alert-danger(ng-show="error") Something went wrong sending your request :( + label + | #{translate("subject")} + .form-group + input.field.text.medium.span8.form-control( + name="subject", + required + ng-model="form.subject", + ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }" + maxlength='255', + tabindex='1', + onkeyup='') + .contact-suggestions(ng-show="suggestions.length") + p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "__kb__", kb: translate("knowledge_base") })} + ul.contact-suggestion-list + li(ng-repeat="suggestion in suggestions") + a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") + span(ng-bind-html="suggestion.name") + i.fa.fa-angle-right + label.desc(ng-show="'"+getUserEmail()+"'.length < 1") + | #{translate("email")} + .form-group(ng-show="'"+getUserEmail()+"'.length < 1") + input.field.text.medium.span8.form-control( + name="email", + required + ng-model="form.email", + ng-init="form.email = '"+getUserEmail()+"'", + type='email', spellcheck='false', + value='', + maxlength='255', + tabindex='2') + label#title12.desc + | #{translate("project_url")} (#{translate("optional")}) + .form-group + input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') + label.desc + | #{translate("contact_message_label")} + .form-group + textarea.field.text.medium.span8.form-control( + name="body", + required + ng-model="form.message", + type='text', + value='', + tabindex='4', + onkeyup='' + ) + .form-group.text-center + input.btn-success.btn.btn-lg( + type='submit', + ng-disabled="contactForm.$invalid || sending", + ng-click="contactUs()" + value=translate("contact_us") + ) span(ng-show="sent") p #{translate("request_sent_thank_you")} diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index c1681d0870..459f081c8a 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product') block scripts - script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false})) + script(src=buildJsPath("libs/angular-1.6.4.min.js", {fingerprint:false})) script. window.sharelatex = { diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index ebeeee6ceb..6007f2e0be 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -37,6 +37,7 @@ div.full-size( keybindings="settings.mode", font-size="settings.fontSize", auto-complete="settings.autoComplete", + auto-pair-delimiters="settings.autoPairDelimiters", spell-check="!anonymous", spell-check-language="project.spellCheckLanguage" highlights="onlineUserCursorHighlights[editor.open_doc_id]" @@ -81,7 +82,7 @@ div.full-size( i.fa.fa-long-arrow-right br a.btn.btn-default.btn-xs( - tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'" + tooltip-html="'"+translate('go_to_pdf_location_in_code', {}, true)+"'" tooltip-placement="right" tooltip-append-to-body="true" ng-click="syncToCode()" diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index 831dc212ae..bf7b9ba331 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -105,6 +105,14 @@ aside#left-menu.full-size( ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" ) + .form-controls + label(for="autoPairDelimiters") #{translate("auto_close_brackets")} + select( + name="autoPairDelimiters" + ng-model="settings.autoPairDelimiters" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + .form-controls.code-check-setting label(for="syntaxValidation") #{translate("syntax_validation")} select( @@ -136,7 +144,8 @@ aside#left-menu.full-size( label(for="fontSize") #{translate("font_size")} select( name="fontSize" - ng-model="settings.fontSize" + ng-model="fontSizeAsStr" + ng-model-options="{ getterSetter: true }" ) each size in ['10','11','12','13','14','16','20','24'] option(value=size) #{size}px diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index b3a3f11948..5429045afb 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -48,7 +48,7 @@ li h2 #{translate("folders")} li.tag( - ng-repeat="tag in tags | orderBy:name", + ng-repeat="tag in tags | orderBy:'name'", ng-class="{active: tag.selected}", ng-cloak, ng-click="selectTag(tag)" diff --git a/services/web/app/views/sentry.pug b/services/web/app/views/sentry.pug index 6406302e0e..cf10c6a329 100644 --- a/services/web/app/views/sentry.pug +++ b/services/web/app/views/sentry.pug @@ -72,6 +72,8 @@ // whitelistUrls: ['example.com/scripts/'] }).install(); } + }, function(err) { + console.log(">> error loading raven", err); }) - if (user && typeof(user) != "undefined" && typeof (user.email) != "undefined") script(type="text/javascript"). diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 8fb00aff31..046903de7b 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -226,8 +226,8 @@ module.exports = settings = # passwordStrengthOptions: # pattern: "aA$3" # length: - # min: 1 - # max: 10 + # min: 6 + # max: 128 # Email support # ------------- diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index f22ecf0219..6f6d9e475e 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -54,6 +54,12 @@ "from": "ansi-regex@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "dev": true + }, "aproba": { "version": "1.1.1", "from": "aproba@>=1.0.3 <2.0.0", @@ -145,6 +151,12 @@ "from": "asynckit@>=0.4.0 <0.5.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" }, + "autoprefixer": { + "version": "6.7.7", + "from": "autoprefixer@>=6.6.1 <7.0.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", + "dev": true + }, "aws-sdk": { "version": "2.41.0", "from": "aws-sdk@>=2.2.36 <3.0.0", @@ -287,25 +299,17 @@ "from": "brace-expansion@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz" }, + "browserslist": { + "version": "1.7.7", + "from": "browserslist@>=1.7.6 <2.0.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", + "dev": true + }, "bson": { "version": "1.0.4", "from": "bson@>=1.0.4 <1.1.0", "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz" }, - "bson-ext": { - "version": "0.1.13", - "from": "bson-ext@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz", - "optional": true, - "dependencies": { - "nan": { - "version": "2.0.9", - "from": "nan@>=2.0.9 <2.1.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz", - "optional": true - } - } - }, "buffer": { "version": "4.9.1", "from": "buffer@4.9.1", @@ -372,6 +376,12 @@ "from": "camelcase@>=1.0.2 <2.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" }, + "caniuse-db": { + "version": "1.0.30000708", + "from": "caniuse-db@>=1.0.30000634 <2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000708.tgz", + "dev": true + }, "caseless": { "version": "0.12.0", "from": "caseless@>=0.12.0 <0.13.0", @@ -392,6 +402,20 @@ "from": "chai-spies@latest", "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-0.7.1.tgz" }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "dev": true, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "dev": true + } + } + }, "character-parser": { "version": "1.2.0", "from": "character-parser@1.2.0", @@ -545,6 +569,12 @@ "from": "cookie-signature@1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" }, + "cookies": { + "version": "0.7.0", + "from": "cookies@>=0.2.2", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.0.tgz", + "dev": true + }, "core-js": { "version": "1.2.7", "from": "core-js@>=1.2.0 <2.0.0", @@ -639,6 +669,13 @@ } } }, + "ctype": { + "version": "0.5.3", + "from": "ctype@0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "dev": true, + "optional": true + }, "dashdash": { "version": "1.14.1", "from": "dashdash@>=1.12.0 <2.0.0", @@ -730,6 +767,38 @@ "from": "doctypes@>=1.1.0 <2.0.0", "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz" }, + "dom-serializer": { + "version": "0.1.0", + "from": "dom-serializer@>=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "dev": true, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "from": "domelementtype@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.0", + "from": "domelementtype@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "dev": true + }, + "domhandler": { + "version": "2.4.1", + "from": "domhandler@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz", + "dev": true + }, + "domutils": { + "version": "1.6.2", + "from": "domutils@>=1.5.1 <2.0.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.6.2.tgz", + "dev": true + }, "dottie": { "version": "1.1.1", "from": "dottie@>=1.0.0 <2.0.0", @@ -761,6 +830,12 @@ "from": "ejs@>=0.8.3 <0.9.0", "resolved": "https://registry.npmjs.org/ejs/-/ejs-0.8.8.tgz" }, + "electron-to-chromium": { + "version": "1.3.16", + "from": "electron-to-chromium@>=1.2.7 <2.0.0", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz", + "dev": true + }, "encoding": { "version": "0.1.12", "from": "encoding@>=0.1.0 <0.2.0", @@ -778,6 +853,18 @@ "from": "end-of-stream@>=0.1.3 <0.2.0", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz" }, + "entities": { + "version": "1.1.1", + "from": "entities@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "dev": true + }, + "es6-promise": { + "version": "4.1.1", + "from": "es6-promise@>=4.0.5 <5.0.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "dev": true + }, "escape-html": { "version": "1.0.2", "from": "escape-html@1.0.2", @@ -887,6 +974,12 @@ "from": "failure@>=1.1.0 <1.2.0", "resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz" }, + "faye-websocket": { + "version": "0.10.0", + "from": "faye-websocket@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "dev": true + }, "file-utils": { "version": "0.1.5", "from": "file-utils@>=0.1.5 <0.2.0", @@ -960,6 +1053,12 @@ "from": "form-data@>=2.1.1 <2.2.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz" }, + "formatio": { + "version": "1.1.1", + "from": "formatio@1.1.1", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", + "dev": true + }, "forwarded": { "version": "0.1.0", "from": "forwarded@>=0.1.0 <0.2.0", @@ -1029,6 +1128,12 @@ "from": "gauge@>=2.7.1 <2.8.0", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz" }, + "gaze": { + "version": "1.1.2", + "from": "gaze@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz", + "dev": true + }, "generic-pool": { "version": "2.4.2", "from": "generic-pool@2.4.2", @@ -1063,6 +1168,26 @@ } } }, + "globule": { + "version": "1.2.0", + "from": "globule@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz", + "dev": true, + "dependencies": { + "glob": { + "version": "7.1.2", + "from": "glob@>=7.1.1 <7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "from": "minimatch@>=3.0.2 <3.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "dev": true + } + } + }, "graceful-fs": { "version": "4.1.11", "from": "graceful-fs@>=4.1.2 <5.0.0", @@ -1130,6 +1255,26 @@ } } }, + "grunt-available-tasks": { + "version": "0.4.1", + "from": "grunt-available-tasks@0.4.1", + "resolved": "https://registry.npmjs.org/grunt-available-tasks/-/grunt-available-tasks-0.4.1.tgz", + "dev": true, + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@>=2.4.0 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "from": "underscore.string@>=2.3.3 <2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "dev": true + } + } + }, "grunt-bunyan": { "version": "0.5.0", "from": "grunt-bunyan@>=0.5.0 <0.6.0", @@ -1142,11 +1287,145 @@ } } }, + "grunt-contrib-clean": { + "version": "0.5.0", + "from": "grunt-contrib-clean@0.5.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-0.5.0.tgz", + "dev": true + }, + "grunt-contrib-coffee": { + "version": "0.10.0", + "from": "grunt-contrib-coffee@0.10.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.10.0.tgz", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "from": "ansi-styles@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "from": "chalk@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "dev": true + }, + "coffee-script": { + "version": "1.7.1", + "from": "coffee-script@>=1.7.0 <1.8.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", + "dev": true + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@>=0.3.5 <0.4.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "from": "strip-ansi@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "dev": true + } + } + }, + "grunt-contrib-less": { + "version": "0.9.0", + "from": "grunt-contrib-less@0.9.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-0.9.0.tgz", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "1.0.0", + "from": "ansi-styles@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "dev": true + }, + "chalk": { + "version": "0.4.0", + "from": "chalk@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "dev": true + }, + "strip-ansi": { + "version": "0.1.1", + "from": "strip-ansi@~0.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "dev": true + } + } + }, + "grunt-contrib-requirejs": { + "version": "0.4.1", + "from": "grunt-contrib-requirejs@0.4.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-requirejs/-/grunt-contrib-requirejs-0.4.1.tgz", + "dev": true + }, + "grunt-contrib-watch": { + "version": "1.0.0", + "from": "grunt-contrib-watch@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.0.0.tgz", + "dev": true, + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.5.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "dev": true + }, + "lodash": { + "version": "3.10.1", + "from": "lodash@>=3.10.1 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "dev": true + } + } + }, + "grunt-env": { + "version": "0.4.4", + "from": "grunt-env@0.4.4", + "resolved": "https://registry.npmjs.org/grunt-env/-/grunt-env-0.4.4.tgz", + "dev": true, + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "dev": true + } + } + }, + "grunt-exec": { + "version": "0.4.7", + "from": "grunt-exec@>=0.4.7 <0.5.0", + "resolved": "https://registry.npmjs.org/grunt-exec/-/grunt-exec-0.4.7.tgz", + "dev": true + }, "grunt-execute": { "version": "0.2.2", "from": "grunt-execute@>=0.2.2 <0.3.0", "resolved": "https://registry.npmjs.org/grunt-execute/-/grunt-execute-0.2.2.tgz" }, + "grunt-file-append": { + "version": "0.0.6", + "from": "grunt-file-append@0.0.6", + "resolved": "https://registry.npmjs.org/grunt-file-append/-/grunt-file-append-0.0.6.tgz", + "dev": true + }, + "grunt-git-rev-parse": { + "version": "0.1.5", + "from": "grunt-git-rev-parse@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-git-rev-parse/-/grunt-git-rev-parse-0.1.5.tgz", + "dev": true + }, "grunt-legacy-log": { "version": "0.1.3", "from": "grunt-legacy-log@>=0.1.0 <0.2.0", @@ -1198,6 +1477,84 @@ } } }, + "grunt-lib-contrib": { + "version": "0.6.1", + "from": "grunt-lib-contrib@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/grunt-lib-contrib/-/grunt-lib-contrib-0.6.1.tgz", + "dev": true + }, + "grunt-mocha-test": { + "version": "0.9.0", + "from": "grunt-mocha-test@0.9.0", + "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.9.0.tgz", + "dev": true + }, + "grunt-newer": { + "version": "1.3.0", + "from": "grunt-newer@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/grunt-newer/-/grunt-newer-1.3.0.tgz", + "dev": true, + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@^1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "dev": true + }, + "glob": { + "version": "7.1.2", + "from": "glob@^7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "from": "minimatch@^3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "dev": true + }, + "rimraf": { + "version": "2.6.1", + "from": "rimraf@>=2.5.2 <3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "dev": true + } + } + }, + "grunt-parallel": { + "version": "0.5.1", + "from": "grunt-parallel@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/grunt-parallel/-/grunt-parallel-0.5.1.tgz", + "dev": true, + "dependencies": { + "q": { + "version": "0.8.12", + "from": "q@>=0.8.12 <0.9.0", + "resolved": "https://registry.npmjs.org/q/-/q-0.8.12.tgz", + "dev": true + } + } + }, + "grunt-postcss": { + "version": "0.8.0", + "from": "grunt-postcss@>=0.8.0 <0.9.0", + "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.8.0.tgz", + "dev": true, + "dependencies": { + "diff": { + "version": "2.2.3", + "from": "diff@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", + "dev": true + } + } + }, + "grunt-sed": { + "version": "0.1.1", + "from": "grunt-sed@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/grunt-sed/-/grunt-sed-0.1.1.tgz", + "dev": true + }, "hang": { "version": "1.0.0", "from": "hang@>=1.0.0 <1.1.0", @@ -1218,6 +1575,24 @@ "from": "has@>=1.0.1 <2.0.0", "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz" }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "dev": true + }, + "has-color": { + "version": "0.1.7", + "from": "has-color@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "from": "has-flag@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "dev": true + }, "has-unicode": { "version": "2.0.1", "from": "has-unicode@>=2.0.0 <3.0.0", @@ -1244,9 +1619,41 @@ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" }, "hooks-fixed": { - "version": "1.1.0", - "from": "hooks-fixed@1.1.0", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz" + "version": "2.0.0", + "from": "hooks-fixed@2.0.0", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz" + }, + "htmlparser2": { + "version": "3.9.2", + "from": "htmlparser2@>=3.9.0 <4.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "dev": true, + "dependencies": { + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "from": "readable-stream@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "from": "safe-buffer@>=5.1.1 <5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "from": "string_decoder@>=1.0.3 <1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "dev": true + } + } }, "http-errors": { "version": "1.6.1", @@ -1263,6 +1670,12 @@ "from": "http-signature@>=1.1.0 <1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" }, + "i18next": { + "version": "1.7.10", + "from": "i18next@>=1.7.1 <1.8.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-1.7.10.tgz", + "dev": true + }, "iconv-lite": { "version": "0.2.11", "from": "iconv-lite@>=0.2.11 <0.3.0", @@ -1285,7 +1698,7 @@ }, "inherits": { "version": "2.0.3", - "from": "inherits@>=2.0.0 <3.0.0", + "from": "inherits@>=2.0.1 <2.1.0", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" }, "ini": { @@ -1398,6 +1811,12 @@ "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", "optional": true }, + "js-base64": { + "version": "2.1.9", + "from": "js-base64@>=2.1.9 <3.0.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz", + "dev": true + }, "js-stringify": { "version": "1.0.2", "from": "js-stringify@>=1.0.1 <2.0.0", @@ -1429,6 +1848,12 @@ "from": "json-stringify-safe@>=5.0.1 <5.1.0", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" }, + "json5": { + "version": "0.2.0", + "from": "json5@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.2.0.tgz", + "dev": true + }, "jsonfile": { "version": "1.1.1", "from": "jsonfile@>=1.1.0 <1.2.0", @@ -1469,23 +1894,15 @@ } }, "kareem": { - "version": "1.0.1", - "from": "kareem@1.0.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz" + "version": "1.5.0", + "from": "kareem@1.5.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.5.0.tgz" }, - "kerberos": { - "version": "0.0.23", - "from": "kerberos@>=0.0.0 <0.1.0", - "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.23.tgz", - "optional": true, - "dependencies": { - "nan": { - "version": "2.5.1", - "from": "nan@>=2.5.1 <2.6.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz", - "optional": true - } - } + "keygrip": { + "version": "1.0.1", + "from": "keygrip@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.1.tgz", + "dev": true }, "kind-of": { "version": "3.1.0", @@ -1628,6 +2045,42 @@ } } }, + "less": { + "version": "1.6.3", + "from": "less@>=1.6.0 <1.7.0", + "resolved": "https://registry.npmjs.org/less/-/less-1.6.3.tgz", + "dev": true, + "dependencies": { + "clean-css": { + "version": "2.0.8", + "from": "clean-css@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-2.0.8.tgz", + "dev": true, + "optional": true + }, + "commander": { + "version": "2.0.0", + "from": "commander@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", + "dev": true, + "optional": true + }, + "mime": { + "version": "1.2.11", + "from": "mime@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "dev": true, + "optional": true + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@~0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "dev": true, + "optional": true + } + } + }, "libbase64": { "version": "0.1.0", "from": "libbase64@0.1.0", @@ -1650,6 +2103,12 @@ "from": "libqp@1.1.0", "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz" }, + "livereload-js": { + "version": "2.2.2", + "from": "livereload-js@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz", + "dev": true + }, "loads": { "version": "0.0.4", "from": "loads@>=0.0.0 <0.1.0", @@ -1862,11 +2321,23 @@ } } }, + "lolex": { + "version": "1.3.2", + "from": "lolex@1.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz", + "dev": true + }, "longest": { "version": "1.0.1", "from": "longest@>=1.0.1 <2.0.0", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz" }, + "lpad": { + "version": "0.1.0", + "from": "lpad@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/lpad/-/lpad-0.1.0.tgz", + "dev": true + }, "lru-cache": { "version": "2.7.3", "from": "lru-cache@>=2.0.0 <3.0.0", @@ -2090,46 +2561,54 @@ } }, "mongoose": { - "version": "4.1.0", - "from": "mongoose@4.1.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz", + "version": "4.11.4", + "from": "mongoose@4.11.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.11.4.tgz", "dependencies": { "async": { - "version": "0.9.0", - "from": "async@0.9.0", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" + "version": "2.1.4", + "from": "async@2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz" }, - "bson": { - "version": "0.3.2", - "from": "bson@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz" + "es6-promise": { + "version": "3.2.1", + "from": "es6-promise@3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "mongodb": { - "version": "2.0.34", - "from": "mongodb@2.0.34", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz" + "version": "2.2.27", + "from": "mongodb@2.2.27", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.27.tgz" }, "mongodb-core": { - "version": "1.2.0", - "from": "mongodb-core@1.2.0", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz", - "dependencies": { - "bson": { - "version": "0.4.23", - "from": "bson@>=0.4.0 <0.5.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz" - } - } + "version": "2.1.11", + "from": "mongodb-core@2.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.11.tgz" }, "ms": { - "version": "0.1.0", - "from": "ms@0.1.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz" + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" }, "readable-stream": { - "version": "1.0.31", - "from": "readable-stream@1.0.31", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz" + "version": "2.2.7", + "from": "readable-stream@2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz" + }, + "safe-buffer": { + "version": "5.1.1", + "from": "safe-buffer@>=5.1.0 <5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" + }, + "string_decoder": { + "version": "1.0.3", + "from": "string_decoder@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz" } } }, @@ -2139,34 +2618,39 @@ "resolved": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz" }, "mpath": { - "version": "0.1.1", - "from": "mpath@0.1.1", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz" + "version": "0.3.0", + "from": "mpath@0.3.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz" }, "mpromise": { - "version": "0.5.4", - "from": "mpromise@0.5.4", - "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz" + "version": "0.5.5", + "from": "mpromise@0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz" }, "mquery": { - "version": "1.6.1", - "from": "mquery@1.6.1", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz", + "version": "2.3.1", + "from": "mquery@2.3.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.1.tgz", "dependencies": { "bluebird": { - "version": "2.9.26", - "from": "bluebird@2.9.26", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz" + "version": "2.10.2", + "from": "bluebird@2.10.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz" }, "debug": { - "version": "2.2.0", - "from": "debug@2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" + "version": "2.6.8", + "from": "debug@2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz" }, "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + }, + "sliced": { + "version": "0.0.5", + "from": "sliced@0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz" } } }, @@ -2208,9 +2692,9 @@ } }, "muri": { - "version": "1.0.0", - "from": "muri@1.0.0", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz" + "version": "1.2.2", + "from": "muri@1.2.2", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz" }, "mv": { "version": "0.0.5", @@ -2244,6 +2728,11 @@ "from": "node-forge@0.2.24", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.2.24.tgz" }, + "node-html-encoder": { + "version": "0.0.2", + "from": "node-html-encoder@0.0.2", + "resolved": "https://registry.npmjs.org/node-html-encoder/-/node-html-encoder-0.0.2.tgz" + }, "node-pre-gyp": { "version": "0.6.30", "from": "node-pre-gyp@0.6.30", @@ -2311,16 +2800,48 @@ "from": "nodemailer-wellknown@0.1.7", "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz" }, + "nomnom": { + "version": "1.6.2", + "from": "nomnom@>=1.6.0 <1.7.0", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", + "dev": true, + "dependencies": { + "colors": { + "version": "0.5.1", + "from": "colors@0.5.x", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", + "dev": true + }, + "underscore": { + "version": "1.4.4", + "from": "underscore@>=1.4.4 <1.5.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "from": "nopt@>=3.0.1 <3.1.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz" }, + "normalize-range": { + "version": "0.1.2", + "from": "normalize-range@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "dev": true + }, "npmlog": { "version": "4.0.2", "from": "npmlog@>=4.0.0 <5.0.0", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.0.2.tgz" }, + "num2fraction": { + "version": "1.2.2", + "from": "num2fraction@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "dev": true + }, "number-is-nan": { "version": "1.0.1", "from": "number-is-nan@>=1.0.0 <2.0.0", @@ -2356,6 +2877,154 @@ "from": "one-time@>=0.0.0 <0.1.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz" }, + "onesky": { + "version": "0.1.6", + "from": "onesky@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/onesky/-/onesky-0.1.6.tgz", + "dev": true, + "dependencies": { + "asn1": { + "version": "0.1.11", + "from": "asn1@0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.1.5", + "from": "assert-plus@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "dev": true, + "optional": true + }, + "async": { + "version": "0.9.2", + "from": "async@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.5.0", + "from": "aws-sign2@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "dev": true, + "optional": true + }, + "boom": { + "version": "0.4.2", + "from": "boom@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", + "dev": true + }, + "combined-stream": { + "version": "0.0.7", + "from": "combined-stream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "dev": true, + "optional": true + }, + "cryptiles": { + "version": "0.2.2", + "from": "cryptiles@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "0.0.5", + "from": "delayed-stream@0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "dev": true, + "optional": true + }, + "forever-agent": { + "version": "0.5.2", + "from": "forever-agent@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", + "dev": true + }, + "form-data": { + "version": "0.1.4", + "from": "form-data@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", + "dev": true, + "optional": true + }, + "hawk": { + "version": "1.1.1", + "from": "hawk@1.1.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.1.1.tgz", + "dev": true, + "optional": true + }, + "hoek": { + "version": "0.9.1", + "from": "hoek@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", + "dev": true + }, + "http-signature": { + "version": "0.10.1", + "from": "http-signature@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "dev": true, + "optional": true + }, + "mime": { + "version": "1.2.11", + "from": "mime@~1.2.11", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", + "dev": true, + "optional": true + }, + "mime-types": { + "version": "1.0.2", + "from": "mime-types@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz", + "dev": true + }, + "node-uuid": { + "version": "1.4.8", + "from": "node-uuid@>=1.4.0 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "dev": true + }, + "oauth-sign": { + "version": "0.3.0", + "from": "oauth-sign@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.3.0.tgz", + "dev": true, + "optional": true + }, + "qs": { + "version": "1.0.2", + "from": "qs@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.0.2.tgz", + "dev": true + }, + "request": { + "version": "2.40.0", + "from": "request@>=2.40.0 <2.41.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.40.0.tgz", + "dev": true + }, + "sntp": { + "version": "0.2.4", + "from": "sntp@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", + "dev": true, + "optional": true + }, + "tunnel-agent": { + "version": "0.4.3", + "from": "tunnel-agent@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "dev": true, + "optional": true + } + } + }, "optimist": { "version": "0.6.1", "from": "optimist@0.6.1", @@ -2479,6 +3148,26 @@ } } }, + "postcss": { + "version": "5.2.17", + "from": "postcss@>=5.2.16 <6.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz", + "dev": true, + "dependencies": { + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.6 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "dev": true + } + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "from": "postcss-value-parser@>=3.2.3 <4.0.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "dev": true + }, "precond": { "version": "0.2.3", "from": "precond@>=0.2.0 <0.3.0", @@ -2935,11 +3624,31 @@ "from": "regexp-clone@0.0.1", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz" }, + "regexp-quote": { + "version": "0.0.0", + "from": "regexp-quote@0.0.0", + "resolved": "https://registry.npmjs.org/regexp-quote/-/regexp-quote-0.0.0.tgz", + "dev": true + }, "repeat-string": { "version": "1.6.1", "from": "repeat-string@>=1.5.2 <2.0.0", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" }, + "replace": { + "version": "0.2.10", + "from": "replace@>=0.2.4 <0.3.0", + "resolved": "https://registry.npmjs.org/replace/-/replace-0.2.10.tgz", + "dev": true, + "dependencies": { + "colors": { + "version": "0.5.1", + "from": "colors@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", + "dev": true + } + } + }, "request": { "version": "2.81.0", "from": "request@>=2.69.0 <3.0.0", @@ -2967,6 +3676,12 @@ "from": "require-like@0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz" }, + "requirejs": { + "version": "2.1.22", + "from": "requirejs@>=2.1.0 <2.2.0", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.1.22.tgz", + "dev": true + }, "requires-port": { "version": "1.0.0", "from": "requires-port@>=1.0.0 <2.0.0", @@ -3035,6 +3750,26 @@ "from": "samsam@1.1.2", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz" }, + "sandboxed-module": { + "version": "0.2.0", + "from": "sandboxed-module@0.2.0", + "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-0.2.0.tgz", + "dev": true, + "dependencies": { + "stack-trace": { + "version": "0.0.6", + "from": "stack-trace@0.0.6", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.6.tgz", + "dev": true + } + } + }, + "sanitize-html": { + "version": "1.14.1", + "from": "sanitize-html@>=1.14.1 <2.0.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.14.1.tgz", + "dev": true + }, "sanitizer": { "version": "0.1.1", "from": "sanitizer@0.1.1", @@ -3195,15 +3930,21 @@ "from": "signal-exit@>=3.0.0 <4.0.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz" }, + "sinon": { + "version": "1.17.7", + "from": "sinon@>=1.17.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", + "dev": true + }, "sixpack-client": { "version": "1.0.0", "from": "sixpack-client@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz" }, "sliced": { - "version": "0.0.5", - "from": "sliced@0.0.5", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz" + "version": "1.0.1", + "from": "sliced@1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz" }, "smtp-connection": { "version": "2.0.1", @@ -3287,6 +4028,12 @@ "from": "strip-json-comments@>=2.0.1 <2.1.0", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" }, + "supports-color": { + "version": "3.2.3", + "from": "supports-color@>=3.2.3 <4.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "dev": true + }, "tar": { "version": "2.2.1", "from": "tar@>=2.2.0 <2.3.0", @@ -3369,6 +4116,78 @@ "from": "timekeeper@", "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-1.0.0.tgz" }, + "tiny-lr": { + "version": "0.2.1", + "from": "tiny-lr@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.2.1.tgz", + "dev": true, + "dependencies": { + "body-parser": { + "version": "1.14.2", + "from": "body-parser@>=1.14.0 <1.15.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "dev": true, + "dependencies": { + "qs": { + "version": "5.2.0", + "from": "qs@5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz", + "dev": true + } + } + }, + "bytes": { + "version": "2.2.0", + "from": "bytes@2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz", + "dev": true + }, + "debug": { + "version": "2.2.0", + "from": "debug@>=2.2.0 <2.3.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dev": true + }, + "http-errors": { + "version": "1.3.1", + "from": "http-errors@>=1.3.1 <1.4.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "dev": true + }, + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", + "dev": true + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "dev": true + }, + "qs": { + "version": "5.1.0", + "from": "qs@>=5.1.0 <5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz", + "dev": true + }, + "raw-body": { + "version": "2.1.7", + "from": "raw-body@>=2.1.5 <2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", + "dev": true, + "dependencies": { + "bytes": { + "version": "2.4.0", + "from": "bytes@2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "dev": true + } + } + } + } + }, "to-mongodb-core": { "version": "2.0.0", "from": "to-mongodb-core@>=2.0.0 <3.0.0", @@ -3406,6 +4225,20 @@ } } }, + "translations-sharelatex": { + "version": "0.1.4", + "from": "git+https://github.com/sharelatex/translations-sharelatex.git#master", + "resolved": "git+https://github.com/sharelatex/translations-sharelatex.git#c2bcc5c8b39034d3dfa2f4509fcc8e1e0be6ed75", + "dev": true, + "dependencies": { + "async": { + "version": "2.5.0", + "from": "async@>=2.1.4 <3.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", + "dev": true + } + } + }, "tsscmp": { "version": "1.0.5", "from": "tsscmp@1.0.5", @@ -3634,6 +4467,18 @@ "from": "void-elements@>=2.0.1 <3.0.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" }, + "websocket-driver": { + "version": "0.6.5", + "from": "websocket-driver@>=0.5.1", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "dev": true + }, + "websocket-extensions": { + "version": "0.1.1", + "from": "websocket-extensions@>=0.1.1", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", + "dev": true + }, "which": { "version": "1.0.9", "from": "which@>=1.0.5 <1.1.0", @@ -3759,6 +4604,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } + }, + "zlib-browserify": { + "version": "0.0.1", + "from": "zlib-browserify@0.0.1", + "resolved": "https://registry.npmjs.org/zlib-browserify/-/zlib-browserify-0.0.1.tgz", + "dev": true } } } diff --git a/services/web/package.json b/services/web/package.json index 9c7c7b58de..14c7f00070 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -30,8 +30,8 @@ "ioredis": "^2.4.0", "jade": "~1.3.1", "ldapjs": "^0.7.1", - "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", "lodash": "^4.13.1", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", "lynx": "0.1.1", "marked": "^0.3.5", "method-override": "^2.3.3", @@ -39,8 +39,9 @@ "mimelib": "0.2.14", "mocha": "1.17.1", "mongojs": "2.4.0", - "mongoose": "4.1.0", + "mongoose": "4.11.4", "multer": "^0.1.8", + "node-html-encoder": "0.0.2", "nodemailer": "2.1.0", "nodemailer-sendgrid-transport": "^0.2.0", "nodemailer-ses-transport": "^1.3.0", @@ -48,23 +49,23 @@ "passport": "^0.3.2", "passport-ldapauth": "^0.6.0", "passport-local": "^1.0.0", + "passport-saml": "^0.15.0", + "pug": "^2.0.0-beta6", "redis": "0.10.1", "redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.2", "request": "^2.69.0", "requests": "^0.1.7", "rimraf": "2.2.6", + "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master", "sanitizer": "0.1.1", "sequelize": "^3.2.0", "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "sixpack-client": "^1.0.0", "temp": "^0.8.3", "underscore": "1.6.0", - "v8-profiler": "^5.2.3", - "xml2js": "0.2.0", - "passport-saml": "^0.15.0", - "pug": "^2.0.0-beta6", "uuid": "^3.0.1", - "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master" + "v8-profiler": "^5.2.3", + "xml2js": "0.2.0" }, "devDependencies": { "autoprefixer": "^6.6.1", diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index b62635170a..41ea0a981b 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -17,7 +17,8 @@ define [ "ErrorCatcher" "localStorage" "ngTagsInput" - ]).config (sixpackProvider)-> + ]).config ($qProvider, sixpackProvider)-> + $qProvider.errorOnUnhandledRejections(false) sixpackProvider.setOptions({ debug: false baseUrl: window.sharelatex.sixpackDomain diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index d9ca11231b..0e6ae19ec2 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -28,16 +28,17 @@ define [ # authentication fails, we will handle it ourselves $http .post(element.attr('action'), formData, {disableAutoLoginRedirect: true}) - .success (data, status, headers, config) -> + .then (httpResponse) -> + { data, status, headers, config } = httpResponse scope[attrs.name].inflight = false response.success = true response.error = false onSuccessHandler = scope[attrs.onSuccess] if onSuccessHandler - onSuccessHandler(data, status, headers, config) + onSuccessHandler(httpResponse) return - + if data.redir? ga('send', 'event', formName, 'success') window.location = data.redir @@ -51,14 +52,15 @@ define [ else ga('send', 'event', formName, 'success') - .error (data, status, headers, config) -> + .catch (httpResponse) -> + { data, status, headers, config } = httpResponse scope[attrs.name].inflight = false response.success = false response.error = true onErrorHandler = scope[attrs.onError] if onErrorHandler - onErrorHandler(data, status, headers, config) + onErrorHandler(httpResponse) return if status == 403 # Forbidden @@ -101,8 +103,8 @@ define [ defaultPasswordOpts = pattern: "" length: - min: 1 - max: 50 + min: 6 + max: 128 allowEmpty: false allowAnyChars: false isMasked: true @@ -125,8 +127,6 @@ define [ [asyncFormCtrl, ngModelCtrl] = ctrl ngModelCtrl.$parsers.unshift (modelValue) -> - - isValid = passField.validatePass() email = asyncFormCtrl.getEmail() || window.usersEmail if !isValid @@ -139,5 +139,8 @@ define [ if opts.length.max? and modelValue.length == opts.length.max isValid = false scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached" + if opts.length.min? and modelValue.length < opts.length.min + isValid = false + scope.complexPasswordErrorMessage = "Password too short, minimum #{opts.length.min}" ngModelCtrl.$setValidity('complexPassword', isValid) return modelValue diff --git a/services/web/public/coffee/directives/selectAll.coffee b/services/web/public/coffee/directives/selectAll.coffee index 6400d2d0ad..4194d050e8 100644 --- a/services/web/public/coffee/directives/selectAll.coffee +++ b/services/web/public/coffee/directives/selectAll.coffee @@ -13,6 +13,7 @@ define [ @clearSelectAllState = () -> $scope.$broadcast "select-all:clear" + return ] link: (scope, element, attrs) -> @@ -63,6 +64,8 @@ define [ ignoreChanges = true scope.$apply () -> scope.ngModel = !scope.ngModel + if !scope.ngModel + selectAllListController.clearSelectAllState() ignoreChanges = false } diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index b1457532b5..f331c4dc80 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -34,7 +34,8 @@ define [ $scope.bibtexPreview.shouldShowDots = false $scope.$apply() $http.get(url) - .success (data) -> + .then (response) -> + { data } = response $scope.bibtexPreview.loading = false $scope.bibtexPreview.error = false # show dots when payload is closs to cutoff @@ -46,7 +47,7 @@ define [ finally $scope.bibtexPreview.data = data $timeout($scope.setHeight, 0) - .error (err) -> + .catch () -> $scope.bibtexPreview.error = true $scope.bibtexPreview.loading = false diff --git a/services/web/public/coffee/ide/chat/services/chatMessages.coffee b/services/web/public/coffee/ide/chat/services/chatMessages.coffee index 6c33f95ea7..e71765266d 100644 --- a/services/web/public/coffee/ide/chat/services/chatMessages.coffee +++ b/services/web/public/coffee/ide/chat/services/chatMessages.coffee @@ -47,7 +47,8 @@ define [ chat.state.loading = true return $http .get(url) - .success (messages = [])-> + .then (response) -> + messages = response.data ? [] chat.state.loading = false if messages.length < MESSAGE_LIMIT chat.state.atEnd = true diff --git a/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee b/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee index c0feaea97a..ebbb99622f 100644 --- a/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee +++ b/services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee @@ -23,12 +23,14 @@ define [ $scope.state.inflight = true $scope.state.error = false cloneProject($scope.inputs.projectName) - .success (data) -> + .then (response) -> + { data } = response window.location = "/project/#{data.project_id}" - .error (body, statusCode) -> + .catch (response) -> + { data, status } = response $scope.state.inflight = false - if statusCode == 400 - $scope.state.error = { message: body } + if status == 400 + $scope.state.error = { message: data } else $scope.state.error = true diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 8d77ea4a9b..1cb527fc40 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -11,6 +11,8 @@ define [ "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" "ide/editor/directives/aceEditor/labels/LabelsManager" "ide/labels/services/labels" + "ide/graphics/services/graphics" + "ide/preamble/services/preamble" ], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) -> EditSession = ace.require('ace/edit_session').EditSession ModeList = ace.require('ace/ext/modelist') @@ -33,9 +35,8 @@ define [ url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" return url - App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels) -> + App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble) -> monkeyPatchSearch($rootScope, $compile) - return { scope: { @@ -44,6 +45,7 @@ define [ keybindings: "=" fontSize: "=" autoComplete: "=" + autoPairDelimiters: "=" sharejsDoc: "=" spellCheck: "=" spellCheckLanguage: "=" @@ -78,10 +80,16 @@ define [ editor = ace.edit(element.find(".ace-editor-body")[0]) editor.$blockScrolling = Infinity - # disable auto insertion of brackets and quotes - editor.setOption('behavioursEnabled', false) + # auto-insertion of braces, brackets, dollars + editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false) editor.setOption('wrapBehavioursEnabled', false) + scope.$watch "autoPairDelimiters", (autoPairDelimiters) => + if autoPairDelimiters + editor.setOption('behavioursEnabled', true) + else + editor.setOption('behavioursEnabled', false) + window.editors ||= [] window.editors.push editor @@ -95,7 +103,7 @@ define [ cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) trackChangesManager = new TrackChangesManager(scope, editor, element) labelsManager = new LabelsManager(scope, editor, element, labels) - autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager) + autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager, graphics, preamble) # Prevert Ctrl|Cmd-S from triggering save dialog 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 cbcdc2eac7..751e534a32 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 @@ -5,6 +5,7 @@ define [ "ace/ext-language_tools" ], (SuggestionManager, SnippetManager) -> Range = ace.require("ace/range").Range + aceSnippetManager = ace.require('ace/snippets').snippetManager getLastCommandFragment = (lineUpToCursor) -> if m = lineUpToCursor.match(/(\\[^\\]+)$/) @@ -12,8 +13,11 @@ define [ else return null + getCommandNameFromFragment = (commandFragment) -> + commandFragment?.match(/\\(\w+)\{/)?[1] + class AutoCompleteManager - constructor: (@$scope, @editor, @element, @labelsManager) -> + constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) -> @suggestionManager = new SuggestionManager() @monkeyPatchAutocomplete() @@ -40,6 +44,37 @@ define [ SnippetCompleter = new SnippetManager() + Graphics = @graphics + Preamble = @preamble + GraphicsCompleter = + getCompletions: (editor, session, pos, prefix, callback) -> + upToCursorRange = new Range(pos.row, 0, pos.row, pos.column) + lineUpToCursor = editor.getSession().getTextRange(upToCursorRange) + commandFragment = getLastCommandFragment(lineUpToCursor) + if commandFragment + match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/) + if match + beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999) + lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange) + needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/) + commandName = match[1] + currentArg = match[3] + graphicsPaths = Preamble.getGraphicsPaths() + result = [] + for graphic in Graphics.getGraphicsFiles() + path = graphic.path + for graphicsPath in graphicsPaths + if path.indexOf(graphicsPath) == 0 + path = path.slice(graphicsPath.length) + break + result.push { + caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}", + meta: "graphic", + score: 50 + } + callback null, result + labelsManager = @labelsManager LabelsCompleter = getCompletions: (editor, session, pos, prefix, callback) -> @@ -59,14 +94,14 @@ define [ caption: "\\#{commandName}{}", snippet: "\\#{commandName}{}", meta: "cross-reference", - score: 11000 + score: 60 } for label in labelsManager.getAllLabels() result.push { caption: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}", value: "\\#{commandName}{#{label}#{if needsClosingBrace then '}' else ''}", meta: "cross-reference", - score: 10000 + score: 50 } callback null, result @@ -93,7 +128,7 @@ define [ caption: "\\#{commandName}{}", snippet: "\\#{commandName}{}", meta: "reference", - score: 11000 + score: 60 } if references.keys and references.keys.length > 0 references.keys.forEach (key) -> @@ -102,13 +137,19 @@ define [ caption: "\\#{commandName}{#{previousArgsCaption}#{key}#{if needsClosingBrace then '}' else ''}", value: "\\#{commandName}{#{previousArgs}#{key}#{if needsClosingBrace then '}' else ''}", meta: "reference", - score: 10000 + score: 50 }) callback null, result else callback null, result - @editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter, LabelsCompleter] + @editor.completers = [ + @suggestionManager, + SnippetCompleter, + ReferencesCompleter, + LabelsCompleter, + GraphicsCompleter + ] disable: () -> @editor.setOptions({ @@ -121,7 +162,18 @@ define [ end = change.end range = new Range(end.row, 0, end.row, end.column) lineUpToCursor = @editor.getSession().getTextRange(range) + if lineUpToCursor.match(/.*%.*/) + return + lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\" + lastTwoChars = lineUpToCursor.slice(-2) + # Don't offer autocomplete on double-backslash, backslash-colon, etc + if lastTwoChars.match(/^\\[^a-z]$/) + @editor?.completer?.detach?() + return commandFragment = getLastCommandFragment(lineUpToCursor) + commandName = getCommandNameFromFragment(commandFragment) + if commandName in ['begin', 'end'] + return # Check that this change was made by us, not a collaborator # (Cursor is still one place behind) # NOTE: this is also the case when a user backspaces over a highlighted region @@ -130,7 +182,7 @@ define [ end.row == cursorPosition.row and end.column == cursorPosition.column + 1 ) - if commandFragment? and commandFragment.length > 2 + if (commandFragment? and commandFragment.length > 2) or lastCharIsBackslash setTimeout () => @editor.execCommand("startAutocomplete") , 0 @@ -154,6 +206,70 @@ define [ if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}" editor.session.remove(range) + # Provide our own `insertMatch` implementation. + # See the `insertMatch` method of Autocomplete in `ext-language_tools.js`. + # We need this to account for editing existing commands, particularly when + # adding a prefix. + # We fix this by detecting when the cursor is in the middle of an existing + # command, and adjusting the insertions/deletions accordingly. + # Example: + # when changing `\ref{}` to `\href{}`, ace default behaviour + # is likely to end up with `\href{}ref{}` + if !data? + completions = this.completions + popup = editor.completer.popup + data = popup.getData(popup.getRow()) + data.completer = + insertMatch: (editor, matchData) -> + for range in editor.selection.getAllRanges() + leftRange = _.clone(range) + rightRange = _.clone(range) + # trim to left of cursor + lineUpToCursor = editor.getSession().getTextRange( + new Range( + range.start.row, + 0, + range.start.row, + range.start.column, + ) + ) + # Delete back to last backslash, as appropriate + lastBackslashIndex = lineUpToCursor.lastIndexOf('\\') + if lastBackslashIndex != -1 + leftRange.start.column = lastBackslashIndex + else + leftRange.start.column -= completions.filterText.length + editor.session.remove(leftRange) + # look at text after cursor + lineBeyondCursor = editor.getSession().getTextRange( + new Range( + rightRange.start.row, + rightRange.start.column, + rightRange.end.row, + 99999 + ) + ) + if lineBeyondCursor + if partialCommandMatch = lineBeyondCursor.match(/^([a-z0-9]+)\{/) + # We've got a partial command after the cursor + commandTail = partialCommandMatch[1] + # remove rest of the partial command, right of cursor + rightRange.end.column += commandTail.length - completions.filterText.length + editor.session.remove(rightRange); + # trim the completion text to just the command, without braces or brackets + # example: '\cite{}' -> '\cite' + if matchData.snippet? + matchData.snippet = matchData.snippet.replace(/[{\[].*[}\]]/, '') + if matchData.caption? + matchData.caption = matchData.caption.replace(/[{\[].*[}\]]/, '') + if matchData.value? + matchData.value = matchData.value.replace(/[{\[].*[}\]]/, '') + # finally, insert the match + if matchData.snippet + aceSnippetManager.insertSnippet(editor, matchData.snippet); + else + editor.execCommand("insertstring", matchData.value || matchData); + Autocomplete::_insertMatch.call this, data # Overwrite this to set autoInsert = false and set font size @@ -166,7 +282,22 @@ define [ editor.completer.autoSelect = true editor.completer.showPopup(editor) editor.completer.cancelContextMenu() - $(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'}) + container = $(editor.completer.popup?.container) + container.css({'font-size': @$scope.fontSize + 'px'}) + # Dynamically set width of autocomplete popup + if filtered = editor?.completer?.completions?.filtered + longestCaption = _.max(filtered.map( (c) -> c.caption.length )) + longestMeta = _.max(filtered.map( (c) -> c.meta.length )) + charWidth = editor.renderer.characterWidth + # between 280 and 700 px + width = Math.max( + Math.min( + Math.round(longestCaption*charWidth + longestMeta*charWidth + 5*charWidth), + 700 + ), + 280 + ) + container.css({width: "#{width}px"}) if editor.completer?.completions?.filtered?.length == 0 editor.completer.detach() bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee index 97241a90ce..250590e4b4 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/SuggestionManager.coffee @@ -1,7 +1,7 @@ define [], () -> class Parser - constructor: (@doc) -> + constructor: (@doc, @prefix) -> parse: () -> # Safari regex is super slow, freezes browser for minutes on end, @@ -10,13 +10,17 @@ define [], () -> if window?._ide?.browserIsSafari limit = 100 - commands = [] + # fully formed commands + realCommands = [] + # commands which match the prefix exactly, + # and could be partially typed or malformed + incidentalCommands = [] seen = {} iterations = 0 while command = @nextCommand() iterations += 1 if limit && iterations > limit - return commands + return realCommands docState = @doc @@ -29,14 +33,23 @@ define [], () -> args++ commandHash = "#{command}\\#{optionalArgs}\\#{args}" - if !seen[commandHash]? - seen[commandHash] = true - commands.push [command, optionalArgs, args] + + if @prefix? && "\\#{command}" == @prefix + incidentalCommands.push [command, optionalArgs, args] + else + if !seen[commandHash]? + seen[commandHash] = true + realCommands.push [command, optionalArgs, args] # Reset to before argument to handle nested commands @doc = docState - return commands + # check incidentals, see if we should pluck out a match + if incidentalCommands.length > 1 + bestMatch = incidentalCommands.sort((a, b) => a[1]+a[2] < b[1]+b[2])[0] + realCommands.push bestMatch + + return realCommands # Ignore single letter commands since auto complete is moot then. commandRegex: /\\([a-zA-Z][a-zA-Z]+)/ @@ -78,12 +91,12 @@ define [], () -> class SuggestionManager getCompletions: (editor, session, pos, prefix, callback) -> doc = session.getValue() - parser = new Parser(doc) + parser = new Parser(doc, prefix) commands = parser.parse() - completions = [] for command in commands caption = "\\#{command[0]}" + score = if caption == prefix then 99 else 50 snippet = caption i = 1 _.times command[1], () -> @@ -94,12 +107,12 @@ define [], () -> snippet += "{${#{i}}}" caption += "{}" i++ - unless caption == prefix - completions.push { - caption: caption - snippet: snippet - meta: "cmd" - } + completions.push { + caption: caption + snippet: snippet + meta: "cmd" + score: score + } callback null, completions diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index 8d23394e39..28964b9090 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -63,11 +63,12 @@ define [ $scope.state.inflight = true ide.fileTreeManager .createDoc(name, parent_folder) - .error (e)-> - $scope.error = e - .success () -> + .then () -> $scope.state.inflight = false $modalInstance.close() + .catch (response)-> + { data } = response + $scope.error = data $scope.cancel = () -> $modalInstance.dismiss('cancel') @@ -95,12 +96,13 @@ define [ $scope.state.inflight = true ide.fileTreeManager .createFolder(name, parent_folder) - .error (e)-> - $scope.error = e - $scope.state.inflight = false - .success () -> + .then () -> $scope.state.inflight = false $modalInstance.close() + .catch (response)-> + { data } = response + $scope.error = data + $scope.state.inflight = false $scope.cancel = () -> $modalInstance.dismiss('cancel') diff --git a/services/web/public/coffee/ide/graphics/services/graphics.coffee b/services/web/public/coffee/ide/graphics/services/graphics.coffee new file mode 100644 index 0000000000..355614b7f4 --- /dev/null +++ b/services/web/public/coffee/ide/graphics/services/graphics.coffee @@ -0,0 +1,17 @@ +define [ + "base" +], (App) -> + + App.factory 'graphics', (ide) -> + + Graphics = + getGraphicsFiles: () -> + graphicsFiles = [] + ide.fileTreeManager.forEachEntity (entity, folder, path) -> + if entity.type == 'file' && entity?.name?.match?(/.*\.(png|jpg|jpeg|pdf|eps)/) + cloned = _.clone(entity) + cloned.path = path + graphicsFiles.push cloned + return graphicsFiles + + return Graphics diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee index 66c855ebc6..6b42714e79 100644 --- a/services/web/public/coffee/ide/history/HistoryManager.coffee +++ b/services/web/public/coffee/ide/history/HistoryManager.coffee @@ -73,7 +73,8 @@ define [ @$scope.history.loading = true @ide.$http .get(url) - .success (data) => + .then (response) => + { data } = response @_loadUpdates(data.updates) @$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp if !data.nextBeforeTimestamp? @@ -109,12 +110,13 @@ define [ @ide.$http .get(url) - .success (data) => + .then (response) => + { data } = response diff.loading = false {text, highlights} = @_parseDiff(data) diff.text = text diff.highlights = highlights - .error () -> + .catch () -> diff.loading = false diff.error = true else diff --git a/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee index d32155395e..544f262d75 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee @@ -9,8 +9,9 @@ define [ .restoreDeletedDoc( $scope.history.diff.doc ) - .success (response) -> - $scope.history.diff.restoredDocNewId = response.doc_id + .then (response) -> + { data } = response + $scope.history.diff.restoredDocNewId = data.doc_id $scope.history.diff.restoreInProgress = false $scope.history.diff.restoreDeletedSuccess = true @@ -37,7 +38,7 @@ define [ $scope.state.inflight = true ide.historyManager .restoreDiff(diff) - .success () -> + .then () -> $scope.state.inflight = false $modalInstance.close() ide.editorManager.openDoc(diff.doc) diff --git a/services/web/public/coffee/ide/labels/services/labels.coffee b/services/web/public/coffee/ide/labels/services/labels.coffee index 3794b309d2..313d48175f 100644 --- a/services/web/public/coffee/ide/labels/services/labels.coffee +++ b/services/web/public/coffee/ide/labels/services/labels.coffee @@ -28,7 +28,8 @@ define [ labels.loadProjectLabelsFromServer = () -> $http .get("/project/#{window.project_id}/labels") - .success (data) -> + .then (response) -> + { data } = response if data.projectLabels for docId, docLabels of data.projectLabels state.documents[docId] = docLabels @@ -39,6 +40,5 @@ define [ "/project/#{window.project_id}/doc/#{docId}/labels", {_csrf: window.csrfToken} ) - .success (data) -> return labels diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 6c4d7973c7..675513e330 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -385,11 +385,13 @@ define [ options.rootDocOverride_id = getRootDocOverride_id() sendCompileRequest(options) - .success (data) -> + .then (response) -> + { data } = response $scope.pdf.view = "pdf" $scope.pdf.compiling = false parseCompileResponse(data) - .error (err, status) -> + .catch (response) -> + { data, status } = response if status == 429 $scope.pdf.rateLimited = true $scope.pdf.compiling = false @@ -514,9 +516,11 @@ define [ clsiserverid:ide.clsiServerId } }) - .success (data) -> + .then (response) -> + { data } = response deferred.resolve(data.pdf or []) - .error (error) -> + .catch (response) -> + error = response.data deferred.reject(error) return deferred.promise @@ -559,12 +563,14 @@ define [ clsiserverid:ide.clsiServerId } }) - .success (data) -> + .then (response) -> + { data } = response if data.code? and data.code.length > 0 doc = ide.fileTreeManager.findEntityByPath(data.code[0].file) return if !doc? deferred.resolve({doc: doc, line: data.code[0].line}) - .error (error) -> + .catch (response) -> + error = response.data deferred.reject(error) return deferred.promise diff --git a/services/web/public/coffee/ide/preamble/services/preamble.coffee b/services/web/public/coffee/ide/preamble/services/preamble.coffee new file mode 100644 index 0000000000..f95cfd9bbe --- /dev/null +++ b/services/web/public/coffee/ide/preamble/services/preamble.coffee @@ -0,0 +1,22 @@ +define [ + "base" +], (App) -> + + App.factory 'preamble', (ide) -> + + Preamble = + getPreambleText: () -> + text = ide.editorManager.getCurrentDocValue().slice(0, 5000) + preamble = text.match(/([^]*)^\\begin\{document\}/m)?[1] || "" + return preamble + + getGraphicsPaths: () -> + preamble = Preamble.getPreambleText() + graphicsPathsArgs = preamble.match(/\\graphicspath\{(.*)\}/)?[1] || "" + paths = [] + re = /\{([^}]*)\}/g + while match = re.exec(graphicsPathsArgs) + paths.push(match[1]) + return paths + + return Preamble diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 5a56baba2c..e4d7855ed9 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -188,7 +188,8 @@ define [ refreshRanges = () -> $http.get "/project/#{$scope.project_id}/ranges" - .success (docs) -> + .then (response) -> + docs = response.data for doc in docs if !$scope.reviewPanel.overview.docsCollapsedState[doc.id]? $scope.reviewPanel.overview.docsCollapsedState[doc.id] = false @@ -438,7 +439,7 @@ define [ thread.submitting = true $scope.$broadcast "comment:add", thread_id, offset, length $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) - .error (error) -> + .catch () -> ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") $scope.$broadcast "editor:clearSelection" $timeout () -> @@ -458,7 +459,7 @@ define [ thread_id = entry.thread_id content = entry.replyContent $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) - .error (error) -> + .catch () -> ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") trackingMetadata = @@ -598,7 +599,8 @@ define [ _refreshingRangeUsers = true $http.get "/project/#{$scope.project_id}/changes/users" - .success (users) -> + .then (response) -> + users = response.data _refreshingRangeUsers = false $scope.users = {} # Always include ourself, since if we submit an op, we might need to display info @@ -608,7 +610,7 @@ define [ for user in users if user.id? $scope.users[user.id] = formatUser(user) - .error () -> + .catch () -> _refreshingRangeUsers = false _threadsLoaded = false @@ -619,7 +621,8 @@ define [ _threadsLoaded = true $scope.reviewPanel.loadingThreads = true $http.get "/project/#{$scope.project_id}/threads" - .success (threads) -> + .then (response) -> + threads = response.data $scope.reviewPanel.loadingThreads = false for thread_id, _ of $scope.reviewPanel.resolvedThreadIds delete $scope.reviewPanel.resolvedThreadIds[thread_id] diff --git a/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee b/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee index 2163bbc8cb..b70b8fc89c 100644 --- a/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/ProjectNameController.coffee @@ -24,13 +24,13 @@ define [ oldName = $scope.project.name $scope.project.name = newName settings.saveProjectSettings({name: $scope.project.name}) - .error (response, statusCode) -> + .catch (response) -> + { data, status } = response $scope.project.name = oldName - if statusCode == 400 - ide.showGenericMessageModal("Error renaming project", response) + if status == 400 + ide.showGenericMessageModal("Error renaming project", data) 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/controllers/SettingsController.coffee b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee index bde479991b..26520d76e0 100644 --- a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee @@ -8,6 +8,11 @@ define [ if $scope.settings.pdfViewer not in ["pdfjs", "native"] $scope.settings.pdfViewer = "pdfjs" + $scope.fontSizeAsStr = (newVal) -> + if newVal? + $scope.settings.fontSize = newVal + return $scope.settings.fontSize.toString() + $scope.$watch "settings.theme", (theme, oldTheme) => if theme != oldTheme settings.saveSettings({theme: theme}) @@ -24,6 +29,10 @@ define [ if autoComplete != oldAutoComplete settings.saveSettings({autoComplete: autoComplete}) + $scope.$watch "settings.autoPairDelimiters", (autoPairDelimiters, oldAutoPairDelimiters) => + if autoPairDelimiters != oldAutoPairDelimiters + settings.saveSettings({autoPairDelimiters: autoPairDelimiters}) + $scope.$watch "settings.pdfViewer", (pdfViewer, oldPdfViewer) => if pdfViewer != oldPdfViewer settings.saveSettings({pdfViewer: pdfViewer}) @@ -61,4 +70,4 @@ define [ $scope.$apply () => $scope.project.spellCheckLanguage = languageCode delete @ignoreUpdates - ] \ No newline at end of file + ] diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 9fdf4d31e2..1ac3bf8709 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -15,16 +15,18 @@ define [ ide.socket.on 'project:membership:changed', (data) => if data.members projectMembers.getMembers() - .success (responseData) => - if responseData.members - $scope.project.members = responseData.members - .error (responseDate) => + .then (response) => + { data } = response + if data.members + $scope.project.members = data.members + .catch () => console.error "Error fetching members for project" if data.invites projectInvites.getInvites() - .success (responseData) => - if responseData.invites - $scope.project.invites = responseData.invites - .error (responseDate) => + .then (response) => + { data } = response + if data.invites + $scope.project.invites = data.invites + .catch () => console.error "Error fetching invites for project" ] diff --git a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee index 6ab15de766..86588d54b2 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -34,7 +34,8 @@ define [ $scope.autocompleteContacts = [] do loadAutocompleteUsers = () -> $http.get "/user/contacts" - .success (data) -> + .then (response) -> + { data } = response $scope.autocompleteContacts = data.contacts or [] for contact in $scope.autocompleteContacts if contact.type == "user" @@ -102,7 +103,8 @@ define [ request = projectInvites.sendInvite(email, $scope.inputs.privileges) request - .success (data) -> + .then (response) -> + { data } = response if data.error $scope.state.error = true $scope.state.errorReason = "#{data.error}" @@ -125,7 +127,7 @@ define [ # with new collaborator information. addNextMember() , 0 - .error () -> + .catch () -> $scope.state.inflight = false $scope.state.error = true $scope.state.errorReason = null @@ -137,12 +139,12 @@ define [ $scope.state.inflight = true projectMembers .removeMember(member) - .success () -> + .then () -> $scope.state.inflight = false index = $scope.project.members.indexOf(member) return if index == -1 $scope.project.members.splice(index, 1) - .error () -> + .catch () -> $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" @@ -151,12 +153,12 @@ define [ $scope.state.inflight = true projectInvites .revokeInvite(invite._id) - .success () -> + .then () -> $scope.state.inflight = false index = $scope.project.invites.indexOf(invite) return if index == -1 $scope.project.invites.splice(index, 1) - .error () -> + .catch () -> $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" @@ -165,10 +167,10 @@ define [ $scope.state.inflight = true projectInvites .resendInvite(invite._id) - .success () -> + .then () -> $scope.state.inflight = false event.target.blur() - .error () -> + .catch () -> $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong resending the invite :(" event.target.blur() diff --git a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee index 2aa4efb505..f046c6028a 100644 --- a/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee +++ b/services/web/public/coffee/ide/wordcount/controllers/WordCountModalController.coffee @@ -11,10 +11,11 @@ define [ params: clsiserverid:ide.clsiServerId $http opts - .success (data) -> + .then (response) -> + { data } = response $scope.status.loading = false $scope.data = data.texcount - .error () -> + .catch () -> $scope.status.error = true $scope.cancel = () -> diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index 307d0307b6..533891548b 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -9,7 +9,6 @@ define [ "libs/fineuploader" "libs/angular-sanitize-1.2.17" "libs/angular-cookie" - "libs/angular-cookies" "libs/passfield" "libs/sixpack" "libs/groove" diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index 2f1b8208e7..54cd8de1ea 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -12,10 +12,10 @@ define [ headers: "X-CSRF-Token": window.csrfToken }) - .success () -> + .then () -> $scope.unsubscribing = false $scope.subscribed = false - .error () -> + .catch () -> $scope.unsubscribing = true $scope.deleteAccount = () -> @@ -59,7 +59,7 @@ define [ password: $scope.state.password disableAutoLoginRedirect: true # we want to handle errors ourselves }) - .success () -> + .then () -> $modalInstance.close() $scope.state.inflight = false $scope.state.error = false @@ -69,7 +69,8 @@ define [ window.location = "/login" , 1000 ) - .error (data, status) -> + .catch (response) -> + { data, status } = response $scope.state.inflight = false if status == 403 $scope.state.invalidCredentials = true diff --git a/services/web/public/coffee/main/announcements.coffee b/services/web/public/coffee/main/announcements.coffee index ed808b5ecc..5663be9ca6 100644 --- a/services/web/public/coffee/main/announcements.coffee +++ b/services/web/public/coffee/main/announcements.coffee @@ -8,9 +8,9 @@ define [ newItems: 0 refreshAnnouncements = -> - $http.get("/announcements").success (announcements) -> - $scope.announcements = announcements - $scope.ui.newItems = _.filter(announcements, (announcement) -> !announcement.read).length + $http.get("/announcements").then (response) -> + $scope.announcements = response.data + $scope.ui.newItems = _.filter($scope.announcements, (announcement) -> !announcement.read).length markAnnouncementsAsRead = -> event_tracking.sendMB "announcement-alert-dismissed", { blogPostId: $scope.announcements[0].id } diff --git a/services/web/public/coffee/main/annual-upgrade.coffee b/services/web/public/coffee/main/annual-upgrade.coffee index 9e16c501f6..838f3590d1 100644 --- a/services/web/public/coffee/main/annual-upgrade.coffee +++ b/services/web/public/coffee/main/annual-upgrade.coffee @@ -22,7 +22,7 @@ define [ $http.post(MESSAGES_URL, body) - .success -> + .then -> $scope.upgradeComplete = true - .error -> + .catch -> console.log "something went wrong changing plan" \ No newline at end of file diff --git a/services/web/public/coffee/main/clear-sessions.coffee b/services/web/public/coffee/main/clear-sessions.coffee index 5524ff8d89..5224bd83ad 100644 --- a/services/web/public/coffee/main/clear-sessions.coffee +++ b/services/web/public/coffee/main/clear-sessions.coffee @@ -11,10 +11,10 @@ define [ $scope.clearSessions = () -> console.log ">> clearing all sessions" $http({method: 'POST', url: "/user/sessions/clear", headers: {'X-CSRF-Token': window.csrfToken}}) - .success () -> + .then () -> $scope.state.otherSessions = [] $scope.state.error = false $scope.state.success = true - .error () -> + .catch () -> $scope.state.error = true ] diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 2f3a2c61e0..7bb86a6b93 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -30,7 +30,7 @@ define [ $scope.suggestions = suggestions $scope.contactUs = -> - if !$scope.form.email? + if !$scope.form.email? or $scope.form.email == "" console.log "email not set" return $scope.sending = true @@ -46,8 +46,16 @@ define [ about: "
form = {{user | json}}+
master = {{master | json}}+
This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) +
+This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) +
+The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. +
+Cached Values
+Cache Info
+{{ $ctrl.log | json }}', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + *
{{ items }}+ *
{{ $ctrl.log | json }}', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } + * }; + * } + * }); + *