diff --git a/services/web/.gitignore b/services/web/.gitignore index a3f982ef53..96ebb86806 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -47,23 +47,21 @@ TpdsWorker.js BackgroundJobsWorker.js UserAndProjectPopulator.coffee -public/js/history/versiondetail.js -!public/js/libs/ -public/js/* -!public/js/ace/* -!public/js/libs/ +public/js/*.js public/js/libs/sharejs.js -public/js/editor.js -public/js/home.js -public/js/forms.js -public/js/gui.js -public/js/admin.js -public/js/history/* +public/js/analytics/ +public/js/directives/ +public/js/filters/ +public/js/ide/ +public/js/main/ +public/js/modules/ +public/js/services/ +public/js/utils/ + public/stylesheets/style.css public/brand/plans.css public/minjs/ -public/js/main.js Gemfile.lock *.swp diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 0e3329c41f..2444c8cb0d 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -1,4 +1,5 @@ fs = require "fs" +PackageVersions = require "./app/coffee/infrastructure/PackageVersions" module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-contrib-coffee' @@ -157,12 +158,13 @@ module.exports = (grunt) -> inlineText: false preserveLicenseComments: false paths: - "moment": "libs/moment-2.9.0" + "moment": "libs/#{PackageVersions.lib('moment')}" "mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML" - "libs/pdf": "libs/pdfjs-1.3.91/pdf" + "pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf" + "ace": "#{PackageVersions.lib('ace')}" shim: - "libs/pdf": - deps: ["libs/pdfjs-1.3.91/compatibility"] + "pdfjs-dist/build/pdf": + deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"] skipDirOptimize: true modules: [ @@ -171,11 +173,13 @@ module.exports = (grunt) -> exclude: ["libs"] }, { name: "ide", - exclude: ["libs", "libs/pdf"] + exclude: ["libs", "pdfjs-dist/build/pdf"] }, { name: "libs" },{ name: "ace/mode-latex" + },{ + name: "ace/worker-latex" } ] @@ -199,7 +203,7 @@ module.exports = (grunt) -> acceptance: src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"] options: - timeout: 10000 + timeout: 40000 reporter: grunt.option('reporter') or 'spec' grep: grunt.option("grep") @@ -380,63 +384,10 @@ module.exports = (grunt) -> grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks) - grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel'] + grunt.registerTask 'run:watch', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel'] + grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec'] grunt.registerTask 'default', 'run' grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed'] - - grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () -> - done = @async() - email = grunt.option("email") - if !email? - console.error "Usage: grunt create-admin-user --email joe@example.com" - process.exit(1) - - settings = require "settings-sharelatex" - UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler" - OneTimeTokenHandler = require "./app/js/Features/Security/OneTimeTokenHandler" - UserRegistrationHandler.registerNewUser { - email: email - password: require("crypto").randomBytes(32).toString("hex") - }, (error, user) -> - if error? and error?.message != "EmailAlreadyRegistered" - throw error - user.isAdmin = true - user.save (error) -> - throw error if error? - ONE_WEEK = 7 * 24 * 60 * 60 # seconds - OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)-> - return next(err) if err? - - console.log "" - console.log """ - Successfully created #{email} as an admin user. - - Please visit the following URL to set a password for #{email} and log in: - - #{settings.siteUrl}/user/password/set?passwordResetToken=#{token} - - """ - done() - - grunt.registerTask 'delete-user', "deletes a user and all their data", () -> - done = @async() - email = grunt.option("email") - if !email? - console.error "Usage: grunt delete-user --email joe@example.com" - process.exit(1) - settings = require "settings-sharelatex" - UserGetter = require "./app/js/Features/User/UserGetter" - UserDeleter = require "./app/js/Features/User/UserDeleter" - UserGetter.getUser email:email, (error, user) -> - if error? - throw error - if !user? - console.log("user #{email} not in database, potentially already deleted") - return done() - UserDeleter.deleteUser user._id, (err)-> - if err? - throw err - done() \ No newline at end of file diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee new file mode 100644 index 0000000000..f9431a0f5c --- /dev/null +++ b/services/web/app/coffee/Features/Analytics/AnalyticsController.coffee @@ -0,0 +1,7 @@ +AnalyticsManager = require "./AnalyticsManager" + +module.exports = AnalyticsController = + recordEvent: (req, res, next) -> + AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) -> + return next(error) if error? + res.send 204 diff --git a/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee new file mode 100644 index 0000000000..f906118b16 --- /dev/null +++ b/services/web/app/coffee/Features/Analytics/AnalyticsManager.coffee @@ -0,0 +1,43 @@ +Settings = require "settings-sharelatex" +logger = require "logger-sharelatex" +_ = require "underscore" + +if !Settings.analytics?.postgres? + module.exports = + recordEvent: (user_id, event, segmentation, callback = () ->) -> + logger.log {user_id, event, segmentation}, "no event tracking configured, logging event" + callback() +else + Sequelize = require "sequelize" + options = _.extend {logging:false}, Settings.analytics.postgres + + sequelize = new Sequelize( + Settings.analytics.postgres.database, + Settings.analytics.postgres.username, + Settings.analytics.postgres.password, + options + ) + + Event = sequelize.define("Event", { + user_id: Sequelize.STRING, + event: Sequelize.STRING, + segmentation: Sequelize.JSONB + }) + + module.exports = + recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) -> + if user_id? and typeof(user_id) != "string" + user_id = user_id.toString() + if user_id == Settings.smokeTest?.userId + # Don't record smoke tests analytics + return callback() + Event + .create({ user_id, event, segmentation }) + .then( + (result) -> callback(), + (error) -> + logger.err {err: error, user_id, event, segmentation}, "error recording analytics event" + callback(error) + ) + + sync: () -> sequelize.sync() \ No newline at end of file diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index d75bef5207..e53ee477b4 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -10,62 +10,115 @@ Settings = require "settings-sharelatex" basicAuth = require('basic-auth-connect') UserHandler = require("../User/UserHandler") UserSessionsManager = require("../User/UserSessionsManager") +Analytics = require "../Analytics/AnalyticsManager" +passport = require 'passport' module.exports = AuthenticationController = - login: (req, res, next = (error) ->) -> - AuthenticationController.doLogin req.body, req, res, next - doLogin: (options, req, res, next) -> - email = options.email?.toLowerCase() - password = options.password - redir = Url.parse(options.redir or "/project").path + serializeUser: (user, callback) -> + lightUser = + _id: user._id + first_name: user.first_name + last_name: user.last_name + isAdmin: user.isAdmin + email: user.email + referal_id: user.referal_id + session_created: (new Date()).toISOString() + ip_address: user._login_req_ip + callback(null, lightUser) + + deserializeUser: (user, cb) -> + cb(null, user) + + passportLogin: (req, res, next) -> + # This function is middleware which wraps the passport.authenticate middleware, + # so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, + # and send a `{redir: ""}` response on success + passport.authenticate('local', (err, user, info) -> + if err? + return next(err) + if user # `user` is either a user object or false + req.login user, (err) -> + # Regenerate the session to get a new sessionID (cookie value) to + # protect against session fixation attacks + oldSession = req.session + req.session.destroy() + req.sessionStore.generate(req) + for key, value of oldSession + req.session[key] = value + # copy to the old `session.user` location, for backward-comptability + req.session.user = req.session.passport.user + req.session.save (err) -> + if err? + logger.err {user_id: user._id}, "error saving regenerated session after login" + return next(err) + UserSessionsManager.trackSession(user, req.sessionID, () ->) + res.json {redir: req._redir} + else + res.json message: info + )(req, res, next) + + doPassportLogin: (req, username, password, done) -> + email = username.toLowerCase() + redir = Url.parse(req?.body?.redir or "/project").path LoginRateLimiter.processLoginRequest email, (err, isAllowed)-> + return done(err) if err? if !isAllowed logger.log email:email, "too many login requests" - res.statusCode = 429 - return res.send - message: - text: req.i18n.translate("to_many_login_requests_2_mins"), - type: 'error' + return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'}) AuthenticationManager.authenticate email: email, password, (error, user) -> - return next(error) if error? + return done(error) if error? if user? - UserHandler.setupLoginData user, -> - LoginRateLimiter.recordSuccessfulLogin email - AuthenticationController._recordSuccessfulLogin user._id - AuthenticationController.establishUserSession req, user, (error) -> - return next(error) if error? - req.session.justLoggedIn = true - logger.log email: email, user_id: user._id.toString(), "successful log in" - res.json redir: redir + # async actions + UserHandler.setupLoginData(user, ()->) + LoginRateLimiter.recordSuccessfulLogin(email) + AuthenticationController._recordSuccessfulLogin(user._id) + Analytics.recordEvent(user._id, "user-logged-in") + logger.log email: email, user_id: user._id.toString(), "successful log in" + req.session.justLoggedIn = true + # capture the request ip for use when creating the session + user._login_req_ip = req.ip + req._redir = redir + return done(null, user) else AuthenticationController._recordFailedLogin() logger.log email: email, "failed log in" - res.json message: - text: req.i18n.translate("email_or_password_wrong_try_again"), - type: 'error' + return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'}) - getLoggedInUserId: (req, callback = (error, user_id) ->) -> - if req?.session?.user?._id? - callback null, req.session.user._id.toString() + setInSessionUser: (req, props) -> + for key, value of props + if req?.session?.passport?.user? + req.session.passport.user[key] = value + if req?.session?.user? + req.session.user[key] = value + + isUserLoggedIn: (req) -> + user_id = AuthenticationController.getLoggedInUserId(req) + return (user_id not in [null, undefined, false]) + + # TODO: perhaps should produce an error if the current user is not present + getLoggedInUserId: (req) -> + user = AuthenticationController.getSessionUser(req) + if user + return user._id else - callback null, null + return null - getLoggedInUser: (req, callback = (error, user) ->) -> - if req.session?.user?._id? - query = req.session.user._id + getSessionUser: (req) -> + if req?.session?.user? + return req.session.user + else if req?.session?.passport?.user + return req.session.passport.user else - return callback null, null - - UserGetter.getUser query, callback + return null requireLogin: () -> doRequest = (req, res, next = (error) ->) -> - if !req.session.user? + if !AuthenticationController.isUserLoggedIn(req) AuthenticationController._redirectToLoginOrRegisterPage(req, res) else - req.user = req.session.user - return next() + req.user = AuthenticationController.getSessionUser(req) + next() return doRequest @@ -79,7 +132,7 @@ module.exports = AuthenticationController = if req.headers['authorization']? return AuthenticationController.httpAuth(req, res, next) - else if req.session.user? + else if AuthenticationController.isUserLoggedIn(req) return next() else logger.log url:req.url, "user trying to access endpoint not in global whitelist" @@ -97,7 +150,6 @@ module.exports = AuthenticationController = else AuthenticationController._redirectToLoginPage(req, res) - _redirectToLoginPage: (req, res) -> logger.log url: req.url, "user not logged in so redirecting to login page" req.query.redir = req.path @@ -124,26 +176,3 @@ module.exports = AuthenticationController = _recordFailedLogin: (callback = (error) ->) -> Metrics.inc "user.login.failed" callback() - - establishUserSession: (req, user, callback = (error) ->) -> - lightUser = - _id: user._id - first_name: user.first_name - last_name: user.last_name - isAdmin: user.isAdmin - email: user.email - referal_id: user.referal_id - session_created: (new Date()).toISOString() - ip_address: req.ip - # Regenerate the session to get a new sessionID (cookie value) to - # protect against session fixation attacks - oldSession = req.session - req.session.destroy() - req.sessionStore.generate(req) - for key, value of oldSession - req.session[key] = value - - req.session.user = lightUser - - UserSessionsManager.trackSession(user, req.sessionID, () ->) - callback() diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee index bfcd55855d..b661455028 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -29,6 +29,9 @@ module.exports = AuthenticationManager = callback null, null setUserPassword: (user_id, password, callback = (error) ->) -> + if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length + return callback("password is too long") + bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) -> return callback(error) if error? bcrypt.hash password, salt, (error, hash) -> diff --git a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee index 4888db0c8a..cb0f583674 100644 --- a/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee +++ b/services/web/app/coffee/Features/Authorization/AuthorizationMiddlewear.coffee @@ -3,6 +3,7 @@ async = require "async" logger = require "logger-sharelatex" ObjectId = require("mongojs").ObjectId Errors = require "../Errors/Errors" +AuthenticationController = require "../Authentication/AuthenticationController" module.exports = AuthorizationMiddlewear = ensureUserCanReadMultipleProjects: (req, res, next) -> @@ -20,7 +21,7 @@ module.exports = AuthorizationMiddlewear = AuthorizationMiddlewear.redirectToRestricted req, res, next else next() - + ensureUserCanReadProject: (req, res, next) -> AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> return next(error) if error? @@ -32,7 +33,7 @@ module.exports = AuthorizationMiddlewear = else logger.log {user_id, project_id}, "denying user read access to project" AuthorizationMiddlewear.redirectToRestricted req, res, next - + ensureUserCanWriteProjectSettings: (req, res, next) -> AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> return next(error) if error? @@ -44,7 +45,7 @@ module.exports = AuthorizationMiddlewear = else logger.log {user_id, project_id}, "denying user write access to project settings" AuthorizationMiddlewear.redirectToRestricted req, res, next - + ensureUserCanWriteProjectContent: (req, res, next) -> AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> return next(error) if error? @@ -56,7 +57,7 @@ module.exports = AuthorizationMiddlewear = else logger.log {user_id, project_id}, "denying user write access to project settings" AuthorizationMiddlewear.redirectToRestricted req, res, next - + ensureUserCanAdminProject: (req, res, next) -> AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) -> return next(error) if error? @@ -68,7 +69,7 @@ module.exports = AuthorizationMiddlewear = else logger.log {user_id, project_id}, "denying user admin access to project" AuthorizationMiddlewear.redirectToRestricted req, res, next - + ensureUserIsSiteAdmin: (req, res, next) -> AuthorizationMiddlewear._getUserId req, (error, user_id) -> return next(error) if error? @@ -90,22 +91,22 @@ module.exports = AuthorizationMiddlewear = AuthorizationMiddlewear._getUserId req, (error, user_id) -> return callback(error) if error? callback(null, user_id, project_id) - + _getUserId: (req, callback = (error, user_id) ->) -> - if req.session?.user?._id? - user_id = req.session.user._id - else - user_id = null - callback null, user_id - + user_id = AuthenticationController.getLoggedInUserId(req) + return callback(null, user_id) + redirectToRestricted: (req, res, next) -> - res.redirect "/restricted" - + res.redirect "/restricted?from=#{encodeURIComponent(req.url)}" + restricted : (req, res, next)-> - if req.session.user? + if AuthenticationController.isUserLoggedIn(req) res.render 'user/restricted', title:'restricted' else - logger.log "user not logged in and trying to access #{req.url}, being redirected to login" - res.redirect '/register' - \ No newline at end of file + from = req.query.from + logger.log {from: from}, "redirecting to login" + redirect_to = "/login" + if from? + redirect_to += "?redir=#{encodeURIComponent(from)}" + res.redirect redirect_to diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee index a986eb8e4c..4e96ce113c 100644 --- a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee @@ -2,14 +2,15 @@ BetaProgramHandler = require './BetaProgramHandler' UserLocator = require "../User/UserLocator" Settings = require "settings-sharelatex" logger = require 'logger-sharelatex' +AuthenticationController = require '../Authentication/AuthenticationController' module.exports = BetaProgramController = optIn: (req, res, next) -> - user_id = req?.session?.user?._id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log {user_id}, "user opting in to beta program" - if !user_id + if !user_id? return next(new Error("no user id in session")) BetaProgramHandler.optIn user_id, (err) -> if err @@ -17,9 +18,9 @@ module.exports = BetaProgramController = return res.redirect "/beta/participate" optOut: (req, res, next) -> - user_id = req?.session?.user?._id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log {user_id}, "user opting out of beta program" - if !user_id + if !user_id? return next(new Error("no user id in session")) BetaProgramHandler.optOut user_id, (err) -> if err @@ -27,7 +28,7 @@ module.exports = BetaProgramController = return res.redirect "/beta/participate" optInPage: (req, res, next)-> - user_id = req.session?.user?._id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log {user_id}, "showing beta participation page for user" UserLocator.findById user_id, (err, user)-> if err diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 4aaee6962a..0f0d9383c7 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/services/web/app/coffee/Features/Blog/BlogController.coffee @@ -28,7 +28,7 @@ module.exports = BlogController = try data = JSON.parse(data) if settings.cdn?.web?.host? - data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'"); + data?.content = data?.content?.replace(/src="(\/[^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'"); catch err logger.err err:err, data:data, "error parsing data from data" res.render "blog/blog_holder", data diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index ef3cbf94fc..35c280712a 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -1,14 +1,18 @@ ChatHandler = require("./ChatHandler") EditorRealTimeController = require("../Editor/EditorRealTimeController") logger = require("logger-sharelatex") +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = - sendMessage: (req, res)-> + sendMessage: (req, res, next)-> project_id = req.params.Project_id - user_id = req.session.user._id messageContent = req.body.content + user_id = AuthenticationController.getLoggedInUserId(req) + if !user_id? + err = new Error('no logged-in user') + return next(err) ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)-> if err? logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee index 1905d0d0a6..a889e9183a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsController.coffee @@ -4,7 +4,9 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler" EditorRealTimeController = require "../Editor/EditorRealTimeController" LimitationsManager = require "../Subscription/LimitationsManager" UserGetter = require "../User/UserGetter" -mimelib = require("mimelib") +EmailHelper = require "../Helpers/EmailHelper" +logger = require 'logger-sharelatex' + module.exports = CollaboratorsController = addUserToProject: (req, res, next) -> @@ -16,11 +18,11 @@ module.exports = CollaboratorsController = return res.json { user: false } else {email, privileges} = req.body - - email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + + email = EmailHelper.parseEmail(email) if !email? or email == "" return res.status(400).send("invalid email address") - + adding_user_id = req.session?.user?._id CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) => return next(error) if error? @@ -35,8 +37,9 @@ module.exports = CollaboratorsController = user_id = req.params.user_id CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) -> return next(error) if error? + EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true} res.sendStatus 204 - + removeSelfFromProject: (req, res, next = (error) ->) -> project_id = req.params.Project_id user_id = req.session?.user?._id @@ -50,3 +53,11 @@ module.exports = CollaboratorsController = EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id) callback() + getAllMembers: (req, res, next) -> + projectId = req.params.Project_id + logger.log {projectId}, "getting all active members for project" + CollaboratorsHandler.getAllMembers projectId, (err, members) -> + if err? + logger.err {projectId}, "error getting members for project" + return next(err) + res.json({members: members}) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee index e9beb1bb43..f669d85de4 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee @@ -2,7 +2,15 @@ Project = require("../../models/Project").Project EmailHandler = require("../Email/EmailHandler") Settings = require "settings-sharelatex" -module.exports = + +module.exports = CollaboratorsEmailHandler = + + _buildInviteUrl: (project, invite) -> + "#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [ + "project_name=#{encodeURIComponent(project.name)}" + "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}" + ].join("&") + notifyUserOfProjectShare: (project_id, email, callback)-> Project .findOne(_id: project_id ) @@ -22,4 +30,19 @@ module.exports = "rs=ci" # referral source = collaborator invite ].join("&") owner: project.owner_ref - EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback \ No newline at end of file + EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback + + notifyUserOfProjectInvite: (project_id, email, invite, callback)-> + Project + .findOne(_id: project_id ) + .select("name owner_ref") + .populate('owner_ref') + .exec (err, project)-> + emailOptions = + to: email + replyTo: project.owner_ref.email + project: + name: project.name + inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite) + owner: project.owner_ref + EmailHandler.sendEmail "projectInvite", emailOptions, callback diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 81557fea42..e974698b18 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -1,6 +1,5 @@ UserCreator = require('../User/UserCreator') Project = require("../../models/Project").Project -mimelib = require("mimelib") logger = require('logger-sharelatex') UserGetter = require "../User/UserGetter" ContactManager = require "../Contacts/ContactManager" @@ -8,8 +7,12 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" async = require "async" PrivilegeLevels = require "../Authorization/PrivilegeLevels" Errors = require "../Errors/Errors" +EmailHelper = require "../Helpers/EmailHelper" +ProjectEditorHandler = require "../Project/ProjectEditorHandler" + module.exports = CollaboratorsHandler = + getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) -> return callback(error) if error? @@ -21,12 +24,12 @@ module.exports = CollaboratorsHandler = for member_id in project.collaberator_refs or [] members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE } return callback null, members - + getMemberIds: (project_id, callback = (error, member_ids) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, members.map (m) -> m.id - + getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? @@ -41,7 +44,7 @@ module.exports = CollaboratorsHandler = (error) -> return callback(error) if error? callback null, result - + getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) -> # In future if the schema changes and getting all member ids is more expensive (multiple documents) # then optimise this. @@ -51,12 +54,12 @@ module.exports = CollaboratorsHandler = if member.id == user_id?.toString() return callback null, member.privilegeLevel return callback null, PrivilegeLevels.NONE - + getMemberCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) -> return callback(error) if error? return callback null, (members or []).length - + getCollaboratorCount: (project_id, callback = (error, count) ->) -> CollaboratorsHandler.getMemberCount project_id, (error, count) -> return callback(error) if error? @@ -69,14 +72,14 @@ module.exports = CollaboratorsHandler = if member.id.toString() == user_id.toString() return callback null, true, member.privilegeLevel return callback null, false, null - + getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) -> Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=> return callback(err) if err? Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=> return callback(err) if err? callback(null, readAndWriteProjects, readOnlyProjects) - + removeUserFromProject: (project_id, user_id, callback = (error) ->)-> logger.log user_id: user_id, project_id: project_id, "removing user" conditions = _id:project_id @@ -86,7 +89,7 @@ module.exports = CollaboratorsHandler = if err? logger.error err: err, "problem removing user from project collaberators" callback(err) - + removeUserFromAllProjets: (user_id, callback = (error) ->) -> CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) -> return callback(error) if error? @@ -98,10 +101,9 @@ module.exports = CollaboratorsHandler = return cb() if !project? CollaboratorsHandler.removeUserFromProject project._id, user_id, cb async.series jobs, callback - + addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) -> - emails = mimelib.parseAddresses(unparsed_email) - email = emails[0]?.address?.toLowerCase() + email = EmailHelper.parseEmail(unparsed_email) if !email? or email == "" return callback(new Error("no valid email provided: '#{unparsed_email}'")) UserCreator.getUserOrCreateHoldingAccount email, (error, user) -> @@ -118,7 +120,7 @@ module.exports = CollaboratorsHandler = existing_users = existing_users.map (u) -> u.toString() if existing_users.indexOf(user_id.toString()) > -1 return callback null # User already in Project - + if privilegeLevel == PrivilegeLevels.READ_AND_WRITE level = {"collaberator_refs":user_id} logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user" @@ -128,11 +130,6 @@ module.exports = CollaboratorsHandler = else return callback(new Error("unknown privilegeLevel: #{privilegeLevel}")) - # Do these in the background - UserGetter.getUser user_id, {email: 1}, (error, user) -> - if error? - logger.error {err: error, project_id, user_id}, "error getting user while adding to project" - CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email ContactManager.addContact adding_user_id, user_id Project.update { _id: project_id }, { $addToSet: level }, (error) -> @@ -143,3 +140,12 @@ module.exports = CollaboratorsHandler = if error? logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator" callback() + + getAllMembers: (projectId, callback=(err, members)->) -> + logger.log {projectId}, "fetching all members" + CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) -> + if error? + logger.err {projectId, error}, "error getting members for project" + return callback(error) + {owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers) + callback(null, members) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee new file mode 100644 index 0000000000..b88ded33b2 --- /dev/null +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -0,0 +1,127 @@ +ProjectGetter = require "../Project/ProjectGetter" +LimitationsManager = require "../Subscription/LimitationsManager" +UserGetter = require "../User/UserGetter" +CollaboratorsHandler = require('./CollaboratorsHandler') +CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') +logger = require('logger-sharelatex') +EmailHelper = require "../Helpers/EmailHelper" +EditorRealTimeController = require("../Editor/EditorRealTimeController") +NotificationsBuilder = require("../Notifications/NotificationsBuilder") +AnalyticsManger = require("../Analytics/AnalyticsManager") +AuthenticationController = require("../Authentication/AuthenticationController") + +module.exports = CollaboratorsInviteController = + + getAllInvites: (req, res, next) -> + projectId = req.params.Project_id + logger.log {projectId}, "getting all active invites for project" + CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) -> + if err? + logger.err {projectId}, "error getting invites for project" + return next(err) + res.json({invites: invites}) + + inviteToProject: (req, res, next) -> + projectId = req.params.Project_id + email = req.body.email + sendingUser = AuthenticationController.getSessionUser(req) + sendingUserId = sendingUser._id + logger.log {projectId, email, sendingUserId}, "inviting to project" + LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) => + return next(error) if error? + if !allowed + logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project" + return res.json {invite: null} + {email, privileges} = req.body + email = EmailHelper.parseEmail(email) + if !email? or email == "" + logger.log {projectId, email, sendingUserId}, "invalid email address" + return res.sendStatus(400) + CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) -> + if err? + logger.err {projectId, email, sendingUserId}, "error creating project invite" + return next(err) + logger.log {projectId, email, sendingUserId}, "invite created" + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true}) + return res.json {invite: invite} + + revokeInvite: (req, res, next) -> + projectId = req.params.Project_id + inviteId = req.params.invite_id + logger.log {projectId, inviteId}, "revoking invite" + CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) -> + if err? + logger.err {projectId, inviteId}, "error revoking invite" + return next(err) + EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true} + res.sendStatus(201) + + resendInvite: (req, res, next) -> + projectId = req.params.Project_id + inviteId = req.params.invite_id + logger.log {projectId, inviteId}, "resending invite" + sendingUser = AuthenticationController.getSessionUser(req) + CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) -> + if err? + logger.err {projectId, inviteId}, "error resending invite" + return next(err) + res.sendStatus(201) + + viewInvite: (req, res, next) -> + projectId = req.params.Project_id + token = req.params.token + _renderInvalidPage = () -> + logger.log {projectId, token}, "invite not valid, rendering not-valid page" + res.render "project/invite/not-valid", {title: "Invalid Invite"} + # check if the user is already a member of the project + currentUser = AuthenticationController.getSessionUser(req) + CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) -> + if err? + logger.err {err, projectId}, "error checking if user is member of project" + return next(err) + if isMember + logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting" + return res.redirect "/project/#{projectId}" + # get the invite + CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) -> + if err? + logger.err {projectId, token}, "error getting invite by token" + return next(err) + # check if invite is gone, or otherwise non-existent + if !invite? + logger.log {projectId, token}, "no invite found for this token" + return _renderInvalidPage() + # check the user who sent the invite exists + UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) -> + if err? + logger.err {err, projectId}, "error getting project owner" + return next(err) + if !owner? + logger.log {projectId}, "no project owner found" + return _renderInvalidPage() + # fetch the project name + ProjectGetter.getProject projectId, {}, (err, project) -> + if err? + logger.err {err, projectId}, "error getting project" + return next(err) + if !project? + logger.log {projectId}, "no project found" + return _renderInvalidPage() + # finally render the invite + res.render "project/invite/show", {invite, project, owner, title: "Project Invite"} + + acceptInvite: (req, res, next) -> + projectId = req.params.Project_id + token = req.params.token + currentUser = AuthenticationController.getSessionUser(req) + logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite" + CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) -> + if err? + logger.err {projectId, token}, "error accepting invite by token" + return next(err) + EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true} + AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id}) + if req.xhr + res.sendStatus 204 # Done async via project page notification + else + res.redirect "/project/#{projectId}" diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee new file mode 100644 index 0000000000..5ed6570c3a --- /dev/null +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -0,0 +1,144 @@ +ProjectInvite = require("../../models/ProjectInvite").ProjectInvite +logger = require('logger-sharelatex') +CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler" +CollaboratorsHandler = require "./CollaboratorsHandler" +UserGetter = require "../User/UserGetter" +ProjectGetter = require "../Project/ProjectGetter" +Async = require "async" +PrivilegeLevels = require "../Authorization/PrivilegeLevels" +Errors = require "../Errors/Errors" +Crypto = require 'crypto' +NotificationsBuilder = require("../Notifications/NotificationsBuilder") + + +module.exports = CollaboratorsInviteHandler = + + getAllInvites: (projectId, callback=(err, invites)->) -> + logger.log {projectId}, "fetching invites for project" + ProjectInvite.find {projectId: projectId}, (err, invites) -> + if err? + logger.err {err, projectId}, "error getting invites from mongo" + return callback(err) + logger.log {projectId, count: invites.length}, "found invites for project" + callback(null, invites) + + getInviteCount: (projectId, callback=(err, count)->) -> + logger.log {projectId}, "counting invites for project" + ProjectInvite.count {projectId: projectId}, (err, count) -> + if err? + logger.err {err, projectId}, "error getting invites from mongo" + return callback(err) + callback(null, count) + + _trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) -> + email = invite.email + UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) -> + if err? + logger.err {projectId, email}, "error checking if user exists" + return callback(err) + if !existingUser? + logger.log {projectId, email}, "no existing user found, returning" + return callback(null) + ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) -> + if err? + logger.err {projectId, email}, "error getting project" + return callback(err) + if !project? + logger.log {projectId}, "no project found while sending notification, returning" + return callback(null) + NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback) + + _tryCancelInviteNotification: (inviteId, callback=()->) -> + NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback) + + _sendMessages: (projectId, sendingUser, invite, callback=(err)->) -> + logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite" + CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)-> + return callback(err) if err? + CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)-> + return callback(err) if err? + callback() + + inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) -> + logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite" + Crypto.randomBytes 24, (err, buffer) -> + if err? + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token" + return callback(err) + token = buffer.toString('hex') + invite = new ProjectInvite { + email: email + token: token + sendingUserId: sendingUser._id + projectId: projectId + privileges: privileges + } + invite.save (err, invite) -> + if err? + logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token" + return callback(err) + # Send email and notification in background + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectId, email}, "error sending messages for invite" + callback(null, invite) + + + revokeInvite: (projectId, inviteId, callback=(err)->) -> + logger.log {projectId, inviteId}, "removing invite" + ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) -> + if err? + logger.err {err, projectId, inviteId}, "error removing invite" + return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->) + callback(null) + + resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) -> + logger.log {projectId, inviteId}, "resending invite email" + ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) -> + if err? + logger.err {err, projectId, inviteId}, "error finding invite" + return callback(err) + if !invite? + logger.err {err, projectId, inviteId}, "no invite found, nothing to resend" + return callback(null) + CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) -> + if err? + logger.err {projectId, inviteId}, "error resending invite messages" + return callback(err) + callback(null) + + getInviteByToken: (projectId, tokenString, callback=(err,invite)->) -> + logger.log {projectId, tokenString}, "fetching invite by token" + ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) -> + if err? + logger.err {err, projectId}, "error fetching invite" + return callback(err) + if !invite? + logger.err {err, projectId, token: tokenString}, "no invite found" + return callback(null, null) + callback(null, invite) + + acceptInvite: (projectId, tokenString, user, callback=(err)->) -> + logger.log {projectId, userId: user._id, tokenString}, "accepting invite" + CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) -> + if err? + logger.err {err, projectId, tokenString}, "error finding invite" + return callback(err) + if !invite + err = new Errors.NotFoundError("no matching invite found") + logger.log {err, projectId, tokenString}, "no matching invite found" + return callback(err) + inviteId = invite._id + CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) -> + if err? + logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project" + return callback(err) + # Remove invite + logger.log {projectId, inviteId}, "removing invite" + ProjectInvite.remove {_id: inviteId}, (err) -> + if err? + logger.err {err, projectId, inviteId}, "error removing invite" + return callback(err) + CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()-> + callback() diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee index 34a6da9a02..4c7cc8c76a 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee @@ -1,6 +1,8 @@ CollaboratorsController = require('./CollaboratorsController') AuthenticationController = require('../Authentication/AuthenticationController') AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear') +CollaboratorsInviteController = require('./CollaboratorsInviteController') +RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear') module.exports = apply: (webRouter, apiRouter) -> @@ -8,3 +10,63 @@ module.exports = webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject + + webRouter.get( + '/project/:Project_id/members', + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsController.getAllMembers + ) + + # invites + webRouter.post( + '/project/:Project_id/invite', + RateLimiterMiddlewear.rateLimit({ + endpointName: "invite-to-project" + params: ["Project_id"] + maxRequests: 200 + timeInterval: 60 * 10 + }), + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.inviteToProject + ) + + webRouter.get( + '/project/:Project_id/invites', + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.getAllInvites + ) + + webRouter.delete( + '/project/:Project_id/invite/:invite_id', + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.revokeInvite + ) + + webRouter.post( + '/project/:Project_id/invite/:invite_id/resend', + RateLimiterMiddlewear.rateLimit({ + endpointName: "resend-invite" + params: ["Project_id"] + maxRequests: 200 + timeInterval: 60 * 10 + }), + AuthenticationController.requireLogin(), + AuthorizationMiddlewear.ensureUserCanAdminProject, + CollaboratorsInviteController.resendInvite + ) + + webRouter.get( + '/project/:Project_id/invite/token/:token', + AuthenticationController.requireLogin(), + CollaboratorsInviteController.viewInvite + ) + + webRouter.post( + '/project/:Project_id/invite/token/:token/accept', + AuthenticationController.requireLogin(), + CollaboratorsInviteController.acceptInvite + ) diff --git a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee index b6270cfce9..c237f212c0 100644 --- a/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiCookieManager.coffee @@ -48,6 +48,10 @@ module.exports = ClsiCookieManager = multi.exec (err)-> callback(err, serverId) + clearServerId: (project_id, callback = (err)->)-> + if !clsiCookiesEnabled + return callback() + rclient.del buildKey(project_id), callback getCookieJar: (project_id, callback = (err, jar)->)-> if !clsiCookiesEnabled diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 568d806e99..08ef23bcd8 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -147,6 +147,7 @@ module.exports = ClsiManager = timeout: options.timeout imageName: project.imageName draft: !!options.draft + check: options.check rootResourcePath: rootResourcePath resources: resources } diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index f3680dc38e..4a23431574 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -16,51 +16,53 @@ module.exports = CompileController = res.setTimeout(5 * 60 * 1000) project_id = req.params.Project_id isAutoCompile = !!req.query?.auto_compile - AuthenticationController.getLoggedInUserId req, (error, user_id) -> + user_id = AuthenticationController.getLoggedInUserId req + options = { + isAutoCompile: isAutoCompile + } + if req.body?.rootDoc_id? + options.rootDoc_id = req.body.rootDoc_id + else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy + options.rootDoc_id = req.body.settingsOverride.rootDoc_id + if req.body?.compiler + options.compiler = req.body.compiler + if req.body?.draft + options.draft = req.body.draft + if req.body?.check in ['validate', 'error', 'silent'] + options.check = req.body.check + logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request" + CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) -> return next(error) if error? - options = { - isAutoCompile: isAutoCompile + res.contentType("application/json") + res.status(200).send JSON.stringify { + status: status + outputFiles: outputFiles + compileGroup: limits?.compileGroup + clsiServerId:clsiServerId + validationProblems:validationProblems } - if req.body?.rootDoc_id? - options.rootDoc_id = req.body.rootDoc_id - else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy - options.rootDoc_id = req.body.settingsOverride.rootDoc_id - if req.body?.compiler - options.compiler = req.body.compiler - if req.body?.draft - options.draft = req.body.draft - logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request" - CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) -> - return next(error) if error? - res.contentType("application/json") - res.status(200).send JSON.stringify { - status: status - outputFiles: outputFiles - compileGroup: limits?.compileGroup - clsiServerId:clsiServerId - validationProblems:validationProblems - } stopCompile: (req, res, next = (error) ->) -> project_id = req.params.Project_id - AuthenticationController.getLoggedInUserId req, (error, user_id) -> + user_id = AuthenticationController.getLoggedInUserId req + logger.log {project_id:project_id, user_id:user_id}, "stop compile request" + CompileManager.stopCompile project_id, user_id, (error) -> return next(error) if error? - logger.log {project_id:project_id, user_id:user_id}, "stop compile request" - CompileManager.stopCompile project_id, user_id, (error) -> - return next(error) if error? - res.status(200).send() + res.status(200).send() _compileAsUser: (req, callback) -> # callback with user_id if per-user, undefined otherwise if not Settings.disablePerUserCompiles - AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id) + user_id = AuthenticationController.getLoggedInUserId req + return callback(null, user_id) else callback() # do a per-project compile, not per-user _downloadAsUser: (req, callback) -> # callback with user_id if per-user, undefined otherwise if not Settings.disablePerUserCompiles - AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id) + user_id = AuthenticationController.getLoggedInUserId req + return callback(null, user_id) else callback() # do a per-project compile, not per-user @@ -149,9 +151,9 @@ module.exports = CompileController = {page, h, v} = req.query if not page?.match(/^\d+$/) return next(new Error("invalid page parameter")) - if not h?.match(/^\d+\.\d+$/) + if not h?.match(/^-?\d+\.\d+$/) return next(new Error("invalid h parameter")) - if not v?.match(/^\d+\.\d+$/) + if not v?.match(/^-?\d+\.\d+$/) return next(new Error("invalid v parameter")) # whether this request is going to a per-user container CompileController._compileAsUser req, (error, user_id) -> diff --git a/services/web/app/coffee/Features/Contacts/ContactController.coffee b/services/web/app/coffee/Features/Contacts/ContactController.coffee index a62c22bbf2..b3c2f7d02b 100644 --- a/services/web/app/coffee/Features/Contacts/ContactController.coffee +++ b/services/web/app/coffee/Features/Contacts/ContactController.coffee @@ -6,33 +6,32 @@ Modules = require "../../infrastructure/Modules" module.exports = ContactsController = getContacts: (req, res, next) -> - AuthenticationController.getLoggedInUserId req, (error, user_id) -> + user_id = AuthenticationController.getLoggedInUserId req + ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) -> return next(error) if error? - ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) -> + UserGetter.getUsers contact_ids, { + email: 1, first_name: 1, last_name: 1, holdingAccount: 1 + }, (error, contacts) -> return next(error) if error? - UserGetter.getUsers contact_ids, { - email: 1, first_name: 1, last_name: 1, holdingAccount: 1 - }, (error, contacts) -> - return next(error) if error? - - # UserGetter.getUsers may not preserve order so put them back in order - positions = {} - for contact_id, i in contact_ids - positions[contact_id] = i - contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()] - # Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) - contacts = contacts.filter (c) -> !c.holdingAccount - - contacts = contacts.map(ContactsController._formatContact) - - Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) -> - return next(error) if error? - contacts = contacts.concat(additional_contacts...) - res.send({ - contacts: contacts - }) - + # UserGetter.getUsers may not preserve order so put them back in order + positions = {} + for contact_id, i in contact_ids + positions[contact_id] = i + contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()] + + # Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) + contacts = contacts.filter (c) -> !c.holdingAccount + + contacts = contacts.map(ContactsController._formatContact) + + Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) -> + return next(error) if error? + contacts = contacts.concat(additional_contacts...) + res.send({ + contacts: contacts + }) + _formatContact: (contact) -> return { id: contact._id?.toString() diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index 379f20fe5b..0c547e53ba 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager") ProjectEditorHandler = require('../Project/ProjectEditorHandler') Metrics = require('../../infrastructure/Metrics') CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler") PrivilegeLevels = require "../Authorization/PrivilegeLevels" module.exports = EditorHttpController = @@ -30,6 +31,7 @@ module.exports = EditorHttpController = ProjectDeleter.unmarkAsDeletedByExternalSource project_id _buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) -> + logger.log {project_id, user_id}, "building the joinProject view" ProjectGetter.getProjectWithoutDocLines project_id, (error, project) -> return callback(error) if error? return callback(new Error("not found")) if !project? @@ -40,10 +42,13 @@ module.exports = EditorHttpController = AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) -> return callback(error) if error? if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE - callback null, null, false - else + logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null" + return callback null, null, false + CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) -> + return callback(error) if error? + logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view" callback(null, - ProjectEditorHandler.buildProjectModelView(project, members), + ProjectEditorHandler.buildProjectModelView(project, members, invites), privilegeLevel ) @@ -135,5 +140,3 @@ module.exports = EditorHttpController = EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) -> return next(error) if error? res.sendStatus 204 - - diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 4bcf0c671d..5ceece048e 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -7,7 +7,7 @@ settings = require("settings-sharelatex") templates = {} -templates.registered = +templates.registered = subject: _.template "Activate your #{settings.appName} Account" layout: PersonalEmailLayout type: "notification" @@ -19,14 +19,14 @@ templates.registered =
If you have any questions or problems, please contact #{settings.adminEmail}.
""" -templates.canceledSubscription = +templates.canceledSubscription = subject: _.template "ShareLaTeX thoughts" layout: PersonalEmailLayout type:"lifecycle" compiledTemplate: _.template '''Hi <%= first_name %>,
-I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?
+I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?
Thank you in advance.
@@ -36,7 +36,7 @@ ShareLaTeX Co-founder ''' -templates.passwordResetRequested = +templates.passwordResetRequested = subject: _.template "Password Reset - #{settings.appName}" layout: NotificationEmailLayout type:"notification" @@ -66,7 +66,7 @@ If you didn't request a password reset, let us know. """ -templates.projectSharedWithYou = +templates.projectSharedWithYou = subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you" layout: NotificationEmailLayout type:"notification" @@ -87,8 +87,33 @@ templates.projectSharedWithYou = """ +templates.projectInvite = + subject: _.template "<%= project.name %> - shared by <%= owner.email %>" + layout: NotificationEmailLayout + type:"notification" + plainTextTemplate: _.template """ +Hi, <%= owner.email %> wants to share '<%= project.name %>' with you. -templates.completeJoinGroupAccount = +Follow this link to view the project: <%= inviteUrl %> + +Thank you + +#{settings.appName} - <%= siteUrl %> +""" + compiledTemplate: _.template """ +Hi, <%= owner.email %> wants to share '<%= project.name %>' with you
+Thank you
+ +""" + +templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" layout: NotificationEmailLayout type:"notification" @@ -121,6 +146,6 @@ module.exports = return { subject : template.subject(opts) html: template.layout(opts) + text: template?.plainTextTemplate?(opts) type:template.type } - diff --git a/services/web/app/coffee/Features/Email/EmailHandler.coffee b/services/web/app/coffee/Features/Email/EmailHandler.coffee index 4bd9586312..048796b09a 100644 --- a/services/web/app/coffee/Features/Email/EmailHandler.coffee +++ b/services/web/app/coffee/Features/Email/EmailHandler.coffee @@ -3,7 +3,7 @@ EmailBuilder = require "./EmailBuilder" EmailSender = require "./EmailSender" if !settings.email? - settings.email = + settings.email = lifecycleEnabled:false @@ -14,6 +14,7 @@ module.exports = if email.type == "lifecycle" and !settings.email.lifecycle return callback() opts.html = email.html + opts.text = email.text opts.subject = email.subject EmailSender.sendEmail opts, (err)-> - callback(err) \ No newline at end of file + callback(err) diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee index cb74502827..8210324747 100644 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ b/services/web/app/coffee/Features/Email/EmailSender.coffee @@ -49,8 +49,11 @@ module.exports = from: defaultFromAddress subject: options.subject html: options.html + text: options.text replyTo: options.replyTo || Settings.email.replyToAddress socketTimeout: 30 * 1000 + if Settings.email.textEncoding? + opts.textEncoding = textEncoding client.sendMail options, (err, res)-> if err? logger.err err:err, "error sending message" diff --git a/services/web/app/coffee/Features/Errors/ErrorController.coffee b/services/web/app/coffee/Features/Errors/ErrorController.coffee index 16b160642a..861a1362b2 100644 --- a/services/web/app/coffee/Features/Errors/ErrorController.coffee +++ b/services/web/app/coffee/Features/Errors/ErrorController.coffee @@ -1,5 +1,6 @@ Errors = require "./Errors" logger = require "logger-sharelatex" +AuthenticationController = require '../Authentication/AuthenticationController' module.exports = ErrorController = notFound: (req, res)-> @@ -11,15 +12,16 @@ module.exports = ErrorController = res.status(500) res.render 'general/500', title: "Server Error" - + handleError: (error, req, res, next) -> + user = AuthenticationController.getSessionUser(req) if error?.code is 'EBADCSRFTOKEN' - logger.warn err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf" + logger.warn err: error,url:req.url, method:req.method, user:user, "invalid csrf" res.sendStatus(403) return if error instanceof Errors.NotFoundError logger.warn {err: error, url: req.url}, "not found error" ErrorController.notFound req, res else - logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear" - ErrorController.serverError req, res \ No newline at end of file + logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear" + ErrorController.serverError req, res diff --git a/services/web/app/coffee/Features/Helpers/EmailHelper.coffee b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee new file mode 100644 index 0000000000..7bc93888fd --- /dev/null +++ b/services/web/app/coffee/Features/Helpers/EmailHelper.coffee @@ -0,0 +1,11 @@ +mimelib = require("mimelib") + + +module.exports = EmailHelper = + + parseEmail: (email) -> + email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase() + if !email? or email == "" + return null + else + return email diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 9f960b1d15..941f4d4d4d 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -1,16 +1,31 @@ logger = require("logger-sharelatex") NotificationsHandler = require("./NotificationsHandler") -module.exports = +module.exports = + + # Note: notification keys should be url-safe groupPlan: (user, licence)-> key : "join-sub-#{licence.subscription_id}" create: (callback = ->)-> - messageOpts = + messageOpts = groupName: licence.name subscription_id: licence.subscription_id - logger.log user_id:user._id, key:key, "creating notification key for user" - NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback + logger.log user_id:user._id, key:@key, "creating notification key for user" + NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback read: (callback = ->)-> NotificationsHandler.markAsReadWithKey user._id, @key, callback + + projectInvite: (invite, project, sendingUser, user) -> + key: "project-invite-#{invite._id}" + create: (callback=()->) -> + messageOpts = + userName: sendingUser.first_name + projectName: project.name + projectId: project._id.toString() + token: invite.token + logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user" + NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback + read: (callback=()->) -> + NotificationsHandler.markAsReadByKeyOnly @key, callback diff --git a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee index c4931f5327..5b83a60248 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsController.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsController.coffee @@ -1,18 +1,20 @@ NotificationsHandler = require("./NotificationsHandler") +AuthenticationController = require("../Authentication/AuthenticationController") logger = require("logger-sharelatex") _ = require("underscore") module.exports = getAllUnreadNotifications: (req, res)-> - NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)-> - unreadNotifications = _.map unreadNotifications, (notification)-> + user_id = AuthenticationController.getLoggedInUserId(req) + NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)-> + unreadNotifications = _.map unreadNotifications, (notification)-> notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) return notification res.send(unreadNotifications) markNotificationAsRead: (req, res)-> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) notification_id = req.params.notification_id NotificationsHandler.markAsRead user_id, notification_id, -> res.send() diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index a7cf6a4672..5a6ca47c2e 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -10,10 +10,10 @@ makeRequest = (opts, callback)-> else request(opts, callback) -module.exports = +module.exports = getUserNotifications: (user_id, callback)-> - opts = + opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" json: true timeout: oneSecond @@ -29,21 +29,25 @@ module.exports = unreadNotifications = [] callback(null, unreadNotifications) - createNotification: (user_id, key, templateKey, messageOpts, callback)-> - opts = - uri: "#{settings.apis.notifications?.url}/user/#{user_id}" - timeout: oneSecond - method:"POST" - json: { + createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)-> + payload = { key:key messageOpts:messageOpts templateKey:templateKey - } + forceCreate: true + } + if expiryDateTime? + payload.expires = expiryDateTime + opts = + uri: "#{settings.apis.notifications?.url}/user/#{user_id}" + timeout: oneSecond + method:"POST" + json: payload logger.log opts:opts, "creating notification for user" makeRequest opts, callback markAsReadWithKey: (user_id, key, callback)-> - opts = + opts = uri: "#{settings.apis.notifications?.url}/user/#{user_id}" method: "DELETE" timeout: oneSecond @@ -52,7 +56,7 @@ module.exports = } logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api" makeRequest opts, callback - + markAsRead: (user_id, notification_id, callback)-> opts = @@ -61,3 +65,13 @@ module.exports = timeout:oneSecond logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api" makeRequest opts, callback + + # removes notification by key, without regard for user_id, + # should not be exposed to user via ui/router + markAsReadByKeyOnly: (key, callback)-> + opts = + uri: "#{settings.apis.notifications?.url}/key/#{key}" + method: "DELETE" + timeout: oneSecond + logger.log {key:key}, "sending mark notification as read with key-only to notifications api" + makeRequest opts, callback diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index d273516056..440534bf11 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -18,6 +18,8 @@ InactiveProjectManager = require("../InactiveData/InactiveProjectManager") ProjectUpdateHandler = require("./ProjectUpdateHandler") ProjectGetter = require("./ProjectGetter") PrivilegeLevels = require("../Authorization/PrivilegeLevels") +AuthenticationController = require("../Authentication/AuthenticationController") +PackageVersions = require("../../infrastructure/PackageVersions") module.exports = ProjectController = @@ -45,10 +47,10 @@ module.exports = ProjectController = async.series jobs, (error) -> return next(error) if error? res.sendStatus(204) - + updateProjectAdminSettings: (req, res, next) -> project_id = req.params.Project_id - + jobs = [] if req.body.publicAccessLevel? jobs.push (callback) -> @@ -88,32 +90,33 @@ module.exports = ProjectController = project_id = req.params.Project_id projectName = req.body.projectName logger.log project_id:project_id, projectName:projectName, "cloning project" - if !req.session.user? + if !AuthenticationController.isUserLoggedIn(req) return res.send redir:"/register" - projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)-> + currentUser = AuthenticationController.getSessionUser(req) + projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)-> if err? - logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project" + logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project" return next(err) res.send(project_id:project._id) newProject: (req, res)-> - user = req.session.user + user_id = AuthenticationController.getLoggedInUserId(req) projectName = req.body.projectName?.trim() template = req.body.template - logger.log user: user, projectType: template, name: projectName, "creating project" + logger.log user: user_id, projectType: template, name: projectName, "creating project" async.waterfall [ (cb)-> if template == 'example' - projectCreationHandler.createExampleProject user._id, projectName, cb + projectCreationHandler.createExampleProject user_id, projectName, cb else - projectCreationHandler.createBasicProject user._id, projectName, cb + projectCreationHandler.createBasicProject user_id, projectName, cb ], (err, project)-> if err? - logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project" + logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project" res.sendStatus 500 else - logger.log project: project, user: user, name: projectName, templateType: template, "created project" + logger.log project: project, user: user_id, name: projectName, templateType: template, "created project" res.send {project_id:project._id} @@ -131,7 +134,8 @@ module.exports = ProjectController = projectListPage: (req, res, next)-> timer = new metrics.Timer("project-list") - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) + currentUser = AuthenticationController.getSessionUser(req) async.parallel { tags: (cb)-> TagsHandler.getAllTags user_id, cb @@ -140,7 +144,7 @@ module.exports = ProjectController = projects: (cb)-> ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb hasSubscription: (cb)-> - LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb + LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb user: (cb) -> User.findById user_id, "featureSwitches", cb }, (err, results)-> @@ -149,7 +153,7 @@ module.exports = ProjectController = return next(err) logger.log results:results, user_id:user_id, "rendering project list" tags = results.tags[0] - notifications = require("underscore").map results.notifications, (notification)-> + notifications = require("underscore").map results.notifications, (notification)-> notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts) return notification projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2] @@ -183,8 +187,8 @@ module.exports = ProjectController = if !Settings.editorIsOpen return res.render("general/closed", {title:"updating_site"}) - if req.session.user? - user_id = req.session.user._id + if AuthenticationController.isUserLoggedIn(req) + user_id = AuthenticationController.getLoggedInUserId(req) anonymous = false else anonymous = true @@ -259,6 +263,7 @@ module.exports = ProjectController = fontSize : user.ace.fontSize autoComplete: user.ace.autoComplete pdfViewer : user.ace.pdfViewer + syntaxValidation: user.ace.syntaxValidation } privilegeLevel: privilegeLevel chatUrl: Settings.apis.chat.url @@ -320,6 +325,7 @@ defaultSettingsForAnonymousUser = (user_id)-> autoComplete: true spellCheckLanguage: "" pdfViewer: "" + syntaxValidation: true subscription: freeTrial: allowed: true @@ -328,8 +334,8 @@ defaultSettingsForAnonymousUser = (user_id)-> THEME_LIST = [] do generateThemeList = () -> - files = fs.readdirSync __dirname + '/../../../../public/js/ace' + files = fs.readdirSync __dirname + '/../../../../public/js/' + PackageVersions.lib('ace') for file in files if file.slice(-2) == "js" and file.match(/^theme-/) cleanName = file.slice(0,-3).slice(6) - THEME_LIST.push cleanName \ No newline at end of file + THEME_LIST.push cleanName diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 4e4991a855..1a10321544 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -1,7 +1,7 @@ _ = require("underscore") module.exports = ProjectEditorHandler = - buildProjectModelView: (project, members) -> + buildProjectModelView: (project, members, invites) -> result = _id : project._id name : project.name @@ -15,17 +15,16 @@ module.exports = ProjectEditorHandler = deletedByExternalDataSource : project.deletedByExternalDataSource || false deletedDocs: project.deletedDocs members: [] - - owner = null - for member in members - if member.privilegeLevel == "owner" - owner = member.user - else - result.members.push @buildUserModelView member.user, member.privilegeLevel - if owner? - result.owner = @buildUserModelView owner, "owner" + invites: invites - result.features = _.defaults(owner?.features or {}, { + if !result.invites? + result.invites = [] + + {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) + result.owner = owner + result.members = members + + result.features = _.defaults(ownerFeatures or {}, { collaborators: -1 # Infinite versioning: false dropbox:false @@ -37,6 +36,18 @@ module.exports = ProjectEditorHandler = return result + buildOwnerAndMembersViews: (members) -> + owner = null + ownerFeatures = null + filteredMembers = [] + for member in members + if member.privilegeLevel == "owner" + ownerFeatures = member.user.features + owner = @buildUserModelView member.user, "owner" + else + filteredMembers.push @buildUserModelView member.user, member.privilegeLevel + {owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers} + buildUserModelView: (user, privileges) -> _id : user._id first_name : user.first_name diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index c68f732f16..ca783a2cbf 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler = if !foundFolder? logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp" @addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)-> + return callback(err) if err? newFolder.parentFolder_id = parentFolder_id previousFolders.push newFolder callback null, previousFolders @@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler = async.reduce folders, [], procesFolder, (err, folders)-> + return callback(err) if err? lastFolder = folders[folders.length-1] folders = _.select folders, (folder)-> !folder.filterOut diff --git a/services/web/app/coffee/Features/Project/ProjectLocator.coffee b/services/web/app/coffee/Features/Project/ProjectLocator.coffee index 9cd80d7c72..44f68123d6 100644 --- a/services/web/app/coffee/Features/Project/ProjectLocator.coffee +++ b/services/web/app/coffee/Features/Project/ProjectLocator.coffee @@ -81,7 +81,7 @@ module.exports = ProjectLocator = else getRootDoc project - findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)-> + findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity, type)->)-> getParentFolder = (haystackFolder, foldersList, level, cb)-> if foldersList.length == 0 @@ -100,12 +100,22 @@ module.exports = ProjectLocator = getEntity = (folder, entityName, cb)-> if !entityName? - return cb null, folder - enteties = _.union folder.fileRefs, folder.docs, folder.folders - result = _.find enteties, (entity)-> - entity?.name.toLowerCase() == entityName.toLowerCase() + return cb null, folder, "folder" + for file in folder.fileRefs or [] + if file?.name.toLowerCase() == entityName.toLowerCase() + result = file + type = "file" + for doc in folder.docs or [] + if doc?.name.toLowerCase() == entityName.toLowerCase() + result = doc + type = "doc" + for childFolder in folder.folders or [] + if childFolder?.name.toLowerCase() == entityName.toLowerCase() + result = childFolder + type = "folder" + if result? - cb null, result + cb null, result, type else cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found") @@ -117,7 +127,7 @@ module.exports = ProjectLocator = if !project? return callback("project could not be found for finding a element #{project_or_id}") if needlePath == '' || needlePath == '/' - return callback(null, project.rootFolder[0]) + return callback(null, project.rootFolder[0], "folder") if needlePath.indexOf('/') == 0 needlePath = needlePath.substring(1) diff --git a/services/web/app/coffee/Features/Referal/ReferalController.coffee b/services/web/app/coffee/Features/Referal/ReferalController.coffee index fbe812a22e..1c8cef03b4 100644 --- a/services/web/app/coffee/Features/Referal/ReferalController.coffee +++ b/services/web/app/coffee/Features/Referal/ReferalController.coffee @@ -1,9 +1,11 @@ logger = require('logger-sharelatex') ReferalHandler = require('./ReferalHandler') +AuthenticationController = require('../Authentication/AuthenticationController') -module.exports = +module.exports = bonus: (req, res)-> - ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)-> + user_id = AuthenticationController.getLoggedInUserId(req) + ReferalHandler.getReferedUserIds user_id, (err, refered_users)-> res.render "referal/bonus", title: "bonus_please_recommend_us" refered_users: refered_users diff --git a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee b/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee deleted file mode 100644 index c6f02d34bf..0000000000 --- a/services/web/app/coffee/Features/Referal/ReferalMiddleware.coffee +++ /dev/null @@ -1,11 +0,0 @@ -User = require("../../models/User").User - -module.exports = RefererMiddleware = - getUserReferalId: (req, res, next) -> - if req.session? and req.session.user? - User.findById req.session.user._id, (error, user) -> - return next(error) if error? - req.session.user.referal_id = user.referal_id - next() - else - next() diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index fa38ddedf6..3ceeab93f8 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -4,7 +4,7 @@ settings = require("settings-sharelatex") ProjectGetter = require "../Project/ProjectGetter" UserGetter = require "../User/UserGetter" DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') -U = require('underscore') +_ = require('underscore') Async = require('async') oneMinInMs = 60 * 1000 @@ -22,24 +22,24 @@ module.exports = ReferencesHandler = _findBibFileIds: (project) -> ids = [] _process = (folder) -> - (folder.fileRefs or []).forEach (file) -> + _.each (folder.fileRefs or []), (file) -> if file?.name?.match(/^.*\.bib$/) ids.push(file._id) - (folder.folders or []).forEach (folder) -> + _.each (folder.folders or []), (folder) -> _process(folder) - (project.rootFolder or []).forEach (rootFolder) -> + _.each (project.rootFolder or []), (rootFolder) -> _process(rootFolder) return ids _findBibDocIds: (project) -> ids = [] _process = (folder) -> - (folder.docs or []).forEach (doc) -> + _.each (folder.docs or []), (doc) -> if doc?.name?.match(/^.*\.bib$/) ids.push(doc._id) - (folder.folders or []).forEach (folder) -> + _.each (folder.folders or []), (folder) -> _process(folder) - (project.rootFolder or []).forEach (rootFolder) -> + _.each (project.rootFolder or []), (rootFolder) -> _process(rootFolder) return ids diff --git a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee index 66581a02b3..b84e1c9b33 100644 --- a/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee +++ b/services/web/app/coffee/Features/Security/OneTimeTokenHandler.coffee @@ -30,5 +30,5 @@ module.exports = multi.get buildKey(token) multi.del buildKey(token) multi.exec (err, results)-> - callback err, results[0] + callback err, results?[0] diff --git a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee index dc71da09fc..f486e94493 100644 --- a/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee +++ b/services/web/app/coffee/Features/Security/RateLimiterMiddlewear.coffee @@ -1,24 +1,22 @@ RateLimiter = require "../../infrastructure/RateLimiter" logger = require "logger-sharelatex" +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = RateLimiterMiddlewear = ### Do not allow more than opts.maxRequests from a single client in opts.timeInterval. Pass an array of opts.params to segment this based on parameters in the request URL, e.g.: - + app.get "/project/:project_id", RateLimiterMiddlewear.rateLimit(endpointName: "open-editor", params: ["project_id"]) - + will rate limit each project_id separately. - + Unique clients are identified by user_id if logged in, and IP address if not. ### rateLimit: (opts) -> return (req, res, next) -> - if req.session?.user? - user_id = req.session.user._id - else - user_id = req.ip + user_id = AuthenticationController.getLoggedInUserId(req) || req.ip params = (opts.params or []).map (p) -> req.params[p] params.push user_id if !opts.endpointName? @@ -37,4 +35,4 @@ module.exports = RateLimiterMiddlewear = logger.warn options, "rate limit exceeded" res.status(429) # Too many requests res.write("Rate limit reached, please try again later") - res.end() \ No newline at end of file + res.end() diff --git a/services/web/app/coffee/Features/Spelling/SpellingController.coffee b/services/web/app/coffee/Features/Spelling/SpellingController.coffee index 973655701b..cff53ec171 100644 --- a/services/web/app/coffee/Features/Spelling/SpellingController.coffee +++ b/services/web/app/coffee/Features/Spelling/SpellingController.coffee @@ -1,13 +1,15 @@ request = require 'request' Settings = require 'settings-sharelatex' logger = require 'logger-sharelatex' +AuthenticationController = require('../Authentication/AuthenticationController') TEN_SECONDS = 1000 * 10 module.exports = SpellingController = proxyRequestToSpellingApi: (req, res, next) -> + user_id = AuthenticationController.getLoggedInUserId(req) url = req.url.slice("/spelling".length) - url = "/user/#{req.session.user._id}#{url}" + url = "/user/#{user_id}#{url}" req.headers["Host"] = Settings.apis.spelling.host request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS) .on "error", (error) -> diff --git a/services/web/app/coffee/Features/StaticPages/HomeController.coffee b/services/web/app/coffee/Features/StaticPages/HomeController.coffee index 3e9ac8439c..6675d55333 100755 --- a/services/web/app/coffee/Features/StaticPages/HomeController.coffee +++ b/services/web/app/coffee/Features/StaticPages/HomeController.coffee @@ -5,12 +5,13 @@ Path = require "path" fs = require "fs" ErrorController = require "../Errors/ErrorController" +AuthenticationController = require('../Authentication/AuthenticationController') homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade") module.exports = HomeController = index : (req,res)-> - if req.session.user + if AuthenticationController.isUserLoggedIn(req) if req.query.scribtex_path? res.redirect "/project?scribtex_path=#{req.query.scribtex_path}" else @@ -33,4 +34,4 @@ module.exports = HomeController = res.render "external/#{page}.jade", title: title else - ErrorController.notFound(req, res, next) \ No newline at end of file + ErrorController.notFound(req, res, next) diff --git a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee index e607c3a020..3caa5ecf5b 100644 --- a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee +++ b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee @@ -1,70 +1,16 @@ -request = require("request") settings = require("settings-sharelatex") logger = require("logger-sharelatex") -_ = require("underscore") -ErrorController = require "../Errors/ErrorController" -StaticPageHelpers = require("./StaticPageHelpers") -sanitize = require('sanitizer') Settings = require("settings-sharelatex") -contentful = require('contentful') -marked = require("marked") +sixpack = require("../../infrastructure/Sixpack") - -module.exports = UniversityController = +module.exports = UniversityController = getPage: (req, res, next)-> - url = req.url?.toLowerCase() - universityUrl = "#{settings.apis.university.url}#{url}" - if StaticPageHelpers.shouldProxy(url) - return UniversityController._directProxy universityUrl, res - - logger.log url:url, "proxying request to university api" - request.get universityUrl, (err, r, data)-> - if r?.statusCode == 404 - return UniversityController.getContentfulPage(req, res, next) - if err? - return res.send 500 - data = data.trim() - try - data = JSON.parse(data) - data.content = data.content.replace(/__ref__/g, sanitize.escape(req.query.ref)) - catch err - logger.err err:err, data:data, "error parsing data from data" - res.render "university/university_holder", data - + url = req.url?.toLowerCase().replace(".html","") + return res.redirect("/i#{url}") getIndexPage: (req, res)-> - req.url = "/university/index.html" - UniversityController.getPage req, res - - _directProxy: (originUrl, res)-> - upstream = request.get(originUrl) - upstream.on "error", (error) -> - logger.error err: error, "university proxy error" - upstream.pipe res - - getContentfulPage: (req, res, next)-> - console.log Settings.contentful - if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken? - return ErrorController.notFound(req, res, next) - - client = contentful.createClient({ - space: Settings.contentful?.uni?.space - accessToken: Settings.contentful?.uni?.accessToken - }) - - url = req.url?.toLowerCase().replace("/university/","") - client.getEntries({content_type: 'caseStudy', 'fields.slug':url}) - .catch (e)-> - return res.send 500 - .then (entry)-> - if !entry? or !entry.items? or entry.items.length == 0 - return ErrorController.notFound(req, res, next) - viewData = entry.items[0].fields - viewData.html = marked(viewData.content) - res.render "university/case_study", viewData:viewData - - + return res.redirect("/i/university") diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index 6c402220a9..59a0748f36 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -4,6 +4,7 @@ User = require("../../models/User").User SubscriptionLocator = require("./SubscriptionLocator") Settings = require("settings-sharelatex") CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler") +CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler") module.exports = @@ -20,10 +21,12 @@ module.exports = return callback(error) if error? CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) => return callback(error) if error? - if current_number + x_collaborators <= allowed_number or allowed_number < 0 - callback null, true - else - callback null, false + CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) => + return callback(error) if error? + if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0 + callback null, true + else + callback null, false userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) -> @userHasSubscription user, (err, hasSubscription, subscription)=> @@ -41,7 +44,7 @@ module.exports = hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true) logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription" callback err, hasValidSubscription, subscription - + userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) -> logger.log user_id: user._id, "checking is user is member of subscription groups" SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) -> @@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)-> return callback(error) if error? User.findById project.owner_ref, (error, owner) -> callback(error, owner) - diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee index 059c5fb02c..2be1fc35c7 100644 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -29,13 +29,15 @@ module.exports = RecurlyWrapper = RecurlyWrapper.apiRequest({ url: "accounts/#{user._id}" method: "GET" + expect404: true }, (error, response, responseBody) -> if error - if response.statusCode == 404 # actually not an error in this case, just no existing account - cache.userExists = false - return next(null, cache) logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account" return next(error) + if response.statusCode == 404 # actually not an error in this case, just no existing account + logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed" + cache.userExists = false + return next(null, cache) logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly" RecurlyWrapper._parseAccountXml responseBody, (err, account) -> if err @@ -236,10 +238,14 @@ module.exports = RecurlyWrapper = "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") "Accept" : "application/xml" "Content-Type" : "application/xml; charset=utf-8" + expect404 = options.expect404 + delete options.expect404 request options, (error, response, body) -> - unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 + unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404) logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly" error = "Recurly API returned with status code: #{response.statusCode}" + if response.statusCode == 404 and expect404 + logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response" callback(error, response, body) sign : (parameters, callback) -> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index ca932ffab3..9b812d8d17 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -13,15 +13,16 @@ module.exports = SubscriptionController = plansPage: (req, res, next) -> plans = SubscriptionViewModelBuilder.buildViewModel() - if !req.session.user? - baseUrl = "/register?redir=" - else + if AuthenticationController.isUserLoggedIn(req) baseUrl = "" + else + baseUrl = "/register?redir=" viewName = "subscriptions/plans" if req.query.v? viewName = "#{viewName}_#{req.query.v}" logger.log viewName:viewName, "showing plans page" GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)-> + return next(err) if err? res.render viewName, title: "plans_and_pricing" plans: plans @@ -31,200 +32,210 @@ module.exports = SubscriptionController = #get to show the recurly.js page paymentPage: (req, res, next) -> - AuthenticationController.getLoggedInUser req, (error, user) => - return next(error) if error? - plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) - LimitationsManager.userHasSubscription user, (err, hasSubscription)-> - return next(err) if err? - if hasSubscription or !plan? - res.redirect "/user/subscription" - else - currency = req.query.currency?.toUpperCase() - GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)-> - return next(err) if err? - if recomendedCurrency? and !currency? - currency = recomendedCurrency - RecurlyWrapper.sign { - subscription: - plan_code : req.query.planCode + user = AuthenticationController.getSessionUser(req) + plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) + LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + return next(err) if err? + if hasSubscription or !plan? + res.redirect "/user/subscription" + else + currency = req.query.currency?.toUpperCase() + GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)-> + return next(err) if err? + if recomendedCurrency? and !currency? + currency = recomendedCurrency + RecurlyWrapper.sign { + subscription: + plan_code : req.query.planCode + currency: currency + account_code: user._id + }, (error, signature) -> + return next(error) if error? + res.render "subscriptions/new", + title : "subscribe" + plan_code: req.query.planCode + currency: currency + countryCode:countryCode + plan:plan + showStudentPlan: req.query.ssp + recurlyConfig: JSON.stringify currency: currency - account_code: user._id - }, (error, signature) -> - return next(error) if error? - res.render "subscriptions/new", - title : "subscribe" - plan_code: req.query.planCode - currency: currency - countryCode:countryCode - plan:plan - showStudentPlan: req.query.ssp - recurlyConfig: JSON.stringify - currency: currency - subdomain: Settings.apis.recurly.subdomain - showCouponField: req.query.scf - showVatField: req.query.svf - couponCode: req.query.cc or "" + subdomain: Settings.apis.recurly.subdomain + showCouponField: req.query.scf + showVatField: req.query.svf + couponCode: req.query.cc or "" userSubscriptionPage: (req, res, next) -> - AuthenticationController.getLoggedInUser req, (error, user) => - return next(error) if error? - LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> - groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) - if subscription?.customAccount - logger.log user: user, "redirecting to custom account page" - res.redirect "/user/subscription/custom_account" - else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember - logger.log user:user, "redirecting to group subscription invite page" - res.redirect groupLicenceInviteUrl - else if !hasSubOrIsGroupMember - logger.log user: user, "redirecting to plans" - res.redirect "/user/subscription/plans" - else - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) -> - return next(error) if error? - logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard" - plans = SubscriptionViewModelBuilder.buildViewModel() - res.render "subscriptions/dashboard", - title: "your_subscription" - recomendedCurrency: subscription?.currency - taxRate:subscription?.taxRate - plans: plans - subscription: subscription || {} - groupSubscriptions: groupSubscriptions - subscriptionTabActive: true - user:user - + user = AuthenticationController.getSessionUser(req) + LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> + return next(err) if err? + groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user) + if subscription?.customAccount + logger.log user: user, "redirecting to custom account page" + res.redirect "/user/subscription/custom_account" + else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember + logger.log user:user, "redirecting to group subscription invite page" + res.redirect groupLicenceInviteUrl + else if !hasSubOrIsGroupMember + logger.log user: user, "redirecting to plans" + res.redirect "/user/subscription/plans" + else + SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) -> + return next(error) if error? + logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard" + plans = SubscriptionViewModelBuilder.buildViewModel() + res.render "subscriptions/dashboard", + title: "your_subscription" + recomendedCurrency: subscription?.currency + taxRate:subscription?.taxRate + plans: plans + subscription: subscription || {} + groupSubscriptions: groupSubscriptions + subscriptionTabActive: true + user:user + saved_billing_details: req.query.saved_billing_details? userCustomSubscriptionPage: (req, res, next)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> - if !subscription? - err = new Error("subscription null for custom account, user:#{user?._id}") - logger.warn err:err, "subscription is null for custom accounts page" - return next(err) - res.render "subscriptions/custom_account", - title: "your_subscription" - subscription: subscription + user = AuthenticationController.getSessionUser(req) + LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)-> + return next(err) if err? + if !subscription? + err = new Error("subscription null for custom account, user:#{user?._id}") + logger.warn err:err, "subscription is null for custom accounts page" + return next(err) + res.render "subscriptions/custom_account", + title: "your_subscription" + subscription: subscription editBillingDetailsPage: (req, res, next) -> - AuthenticationController.getLoggedInUser req, (error, user) -> - return next(error) if error? - LimitationsManager.userHasSubscription user, (err, hasSubscription)-> - if !hasSubscription - res.redirect "/user/subscription" - else - RecurlyWrapper.sign { - account_code: user._id - }, (error, signature) -> - return next(error) if error? - res.render "subscriptions/edit-billing-details", - title : "update_billing_details" - recurlyConfig: JSON.stringify - currency: "USD" - subdomain: Settings.apis.recurly.subdomain - signature : signature - successURL : "#{Settings.siteUrl}/user/subscription/update" - user : - id : user._id + user = AuthenticationController.getSessionUser(req) + LimitationsManager.userHasSubscription user, (err, hasSubscription)-> + return next(err) if err? + if !hasSubscription + res.redirect "/user/subscription" + else + RecurlyWrapper.sign { + account_code: user._id + }, (error, signature) -> + return next(error) if error? + res.render "subscriptions/edit-billing-details", + title : "update_billing_details" + recurlyConfig: JSON.stringify + currency: "USD" + subdomain: Settings.apis.recurly.subdomain + signature : signature + successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update" + user : + id : user._id + + updateBillingDetails: (req, res, next) -> + res.redirect "/user/subscription?saved_billing_details=true" createSubscription: (req, res, next)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - return callback(error) if error? - recurly_token_id = req.body.recurly_token_id - subscriptionDetails = req.body.subscriptionDetails - logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription" - SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong creating subscription" - return res.sendStatus 500 - res.sendStatus 201 + user = AuthenticationController.getSessionUser(req) + recurly_token_id = req.body.recurly_token_id + subscriptionDetails = req.body.subscriptionDetails + logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription" + SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong creating subscription" + return res.sendStatus 500 + res.sendStatus 201 - successful_subscription: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) => - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> - res.render "subscriptions/successful_subscription", - title: "thank_you" - subscription:subscription + successful_subscription: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) -> + return next(error) if error? + res.render "subscriptions/successful_subscription", + title: "thank_you" + subscription:subscription cancelSubscription: (req, res, next) -> - AuthenticationController.getLoggedInUser req, (error, user) -> - logger.log user_id:user._id, "canceling subscription" - return next(error) if error? - SubscriptionHandler.cancelSubscription user, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong canceling subscription" - res.redirect "/user/subscription" - - updateSubscription: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - return next(error) if error? - planCode = req.body.plan_code - logger.log planCode: planCode, user_id:user._id, "updating subscription" - SubscriptionHandler.updateSubscription user, planCode, null, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong updating subscription" - res.redirect "/user/subscription" + user = AuthenticationController.getSessionUser(req) + logger.log user_id:user._id, "canceling subscription" + SubscriptionHandler.cancelSubscription user, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong canceling subscription" + return next(err) + res.redirect "/user/subscription" - reactivateSubscription: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - logger.log user_id:user._id, "reactivating subscription" - return next(error) if error? - SubscriptionHandler.reactivateSubscription user, (err)-> - if err? - logger.err err:err, user_id:user._id, "something went wrong reactivating subscription" - res.redirect "/user/subscription" + updateSubscription: (req, res, next)-> + _origin = req?.query?.origin || null + user = AuthenticationController.getSessionUser(req) + planCode = req.body.plan_code + if !planCode? + err = new Error('plan_code is not defined') + logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form" + return next(err) + logger.log planCode: planCode, user_id:user._id, "updating subscription" + SubscriptionHandler.updateSubscription user, planCode, null, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong updating subscription" + return next(err) + res.redirect "/user/subscription" - recurlyCallback: (req, res)-> + reactivateSubscription: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + logger.log user_id:user._id, "reactivating subscription" + SubscriptionHandler.reactivateSubscription user, (err)-> + if err? + logger.err err:err, user_id:user._id, "something went wrong reactivating subscription" + return next(err) + res.redirect "/user/subscription" + + recurlyCallback: (req, res, next)-> logger.log data: req.body, "received recurly callback" # we only care if a subscription has exipired if req.body? and req.body["expired_subscription_notification"]? recurlySubscription = req.body["expired_subscription_notification"].subscription - SubscriptionHandler.recurlyCallback recurlySubscription, -> + SubscriptionHandler.recurlyCallback recurlySubscription, (err)-> + return next(err) if err? res.sendStatus 200 else res.sendStatus 200 - renderUpgradeToAnnualPlanPage: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> - planCode = subscription?.planCode.toLowerCase() - if planCode?.indexOf("annual") != -1 - planName = "annual" - else if planCode?.indexOf("student") != -1 - planName = "student" - else if planCode?.indexOf("collaborator") != -1 - planName = "collaborator" - if !hasSubscription - return res.redirect("/user/subscription/plans") - logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page" - res.render "subscriptions/upgradeToAnnual", - title: "Upgrade to annual" - planName: planName + renderUpgradeToAnnualPlanPage: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + return next(err) if err? + planCode = subscription?.planCode.toLowerCase() + if planCode?.indexOf("annual") != -1 + planName = "annual" + else if planCode?.indexOf("student") != -1 + planName = "student" + else if planCode?.indexOf("collaborator") != -1 + planName = "collaborator" + if !hasSubscription + return res.redirect("/user/subscription/plans") + logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page" + res.render "subscriptions/upgradeToAnnual", + title: "Upgrade to annual" + planName: planName - processUpgradeToAnnualPlan: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - {planName} = req.body - coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] - annualPlanName = "#{planName}-annual" - logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount" - SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)-> + processUpgradeToAnnualPlan: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + {planName} = req.body + coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName] + annualPlanName = "#{planName}-annual" + logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount" + SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)-> + if err? + logger.err err:err, user_id:user._id, "error updating subscription" + return next(err) + res.sendStatus 200 + + extendTrial: (req, res, next)-> + user = AuthenticationController.getSessionUser(req) + LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> + return next(err) if err? + SubscriptionHandler.extendTrial subscription, 14, (err)-> if err? - logger.err err:err, user_id:user._id, "error updating subscription" - res.sendStatus 500 + res.send 500 else - res.sendStatus 200 - - extendTrial: (req, res)-> - AuthenticationController.getLoggedInUser req, (error, user) -> - LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)-> - SubscriptionHandler.extendTrial subscription, 14, (err)-> - if err? - res.send 500 - else - res.send 200 + res.send 200 recurlyNotificationParser: (req, res, next) -> xml = "" diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee index 5b20608eea..3e49cd20bd 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupController.coffee @@ -3,27 +3,28 @@ logger = require("logger-sharelatex") SubscriptionLocator = require("./SubscriptionLocator") ErrorsController = require("../Errors/ErrorController") SubscriptionDomainHandler = require("./SubscriptionDomainHandler") +AuthenticationController = require('../Authentication/AuthenticationController') _ = require("underscore") async = require("async") module.exports = addUserToGroup: (req, res)-> - adminUserId = req.session.user._id + adminUserId = AuthenticationController.getLoggedInUserId(req) newEmail = req.body?.email?.toLowerCase()?.trim() logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription" SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)-> if err? logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group" return res.sendStatus 500 - result = + result = user:user if err and err.limitReached result.limitReached = true res.json(result) removeUserFromGroup: (req, res)-> - adminUserId = req.session.user._id + adminUserId = AuthenticationController.getLoggedInUserId(req) userToRemove_id = req.params.user_id logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription" SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)-> @@ -31,10 +32,10 @@ module.exports = logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group" return res.sendStatus 500 res.send() - + removeSelfFromGroup: (req, res)-> adminUserId = req.query.admin_user_id - userToRemove_id = req.session.user._id + userToRemove_id = AuthenticationController.getLoggedInUserId(req) logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request" SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)-> if err? @@ -43,7 +44,7 @@ module.exports = res.send() renderSubscriptionGroupAdminPage: (req, res)-> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> if !subscription.groupPlan return res.redirect("/") @@ -55,11 +56,11 @@ module.exports = renderGroupInvitePage: (req, res)-> group_subscription_id = req.params.subscription_id - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id) if !licence? return ErrorsController.notFound(req, res) - jobs = + jobs = partOfGroup: (cb)-> SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb subscription: (cb)-> @@ -77,22 +78,26 @@ module.exports = beginJoinGroup: (req, res)-> subscription_id = req.params.subscription_id - user_id = req.session.user._id + currentUser = AuthenticationController.getSessionUser(req) + if !currentUser? + logger.err {subscription_id}, "error getting current user" + return res.sendStatus 500 licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id) if !licence? return ErrorsController.notFound(req, res) - SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)-> + SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)-> if err? res.sendStatus 500 else res.sendStatus 200 completeJoin: (req, res)-> + currentUser = AuthenticationController.getSessionUser(req) subscription_id = req.params.subscription_id if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)? return ErrorsController.notFound(req, res) - email = req?.session?.user?.email - logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group" + email = currentUser?.email + logger.log subscription_id:subscription_id, user_id:currentUser?._id, email:email, "starting the completion of joining group" SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)-> if err? and err == "token_not_found" return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true" @@ -109,10 +114,10 @@ module.exports = return ErrorsController.notFound(req, res) res.render "subscriptions/group/successful_join", title: "Sucessfully joined group" - licenceName:licence.name + licenceName:licence.name exportGroupCsv: (req, res)-> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log user_id: user_id, "exporting group csv" SubscriptionLocator.getUsersSubscription user_id, (err, subscription)-> if !subscription.groupPlan diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee index 2396dca07b..ba95895dcd 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee @@ -7,6 +7,8 @@ SubscriptionUpdater = require("./SubscriptionUpdater") LimitationsManager = require('./LimitationsManager') EmailHandler = require("../Email/EmailHandler") Events = require "../../infrastructure/Events" +Analytics = require("../Analytics/AnalyticsManager") + module.exports = @@ -52,6 +54,7 @@ module.exports = setTimeout (-> EmailHandler.sendEmail "canceledSubscription", emailOpts ), ONE_HOUR_IN_MS Events.emit "cancelSubscription", user._id + Analytics.recordEvent user._id, "subscription-canceled" callback() else callback() diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee index d73a35a69d..beee4a3158 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionLocator.coffee @@ -14,7 +14,11 @@ module.exports = logger.log user_id:user_id, "got users subscription" callback(err, subscription) - getMemberSubscriptions: (user_id, callback) -> + getMemberSubscriptions: (user_or_id, callback) -> + if user_or_id? and user_or_id._id? + user_id = user_or_id._id + else if user_or_id? + user_id = user_or_id logger.log user_id: user_id, "getting users group subscriptions" Subscription.find(member_ids: user_id).populate("admin_id").exec callback @@ -25,4 +29,4 @@ module.exports = Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback getGroupSubscriptionMemberOf: (user_id, callback)-> - Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback \ No newline at end of file + Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee index f2d66c30c5..62d4d306ab 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee @@ -16,6 +16,7 @@ module.exports = webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage + webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee index e239a779b9..44c31c8d60 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee @@ -4,6 +4,7 @@ PlansLocator = require("./PlansLocator") SubscriptionFormatters = require("./SubscriptionFormatters") LimitationsManager = require("./LimitationsManager") SubscriptionLocator = require("./SubscriptionLocator") +logger = require('logger-sharelatex') _ = require("underscore") module.exports = @@ -16,6 +17,10 @@ module.exports = if subscription? return callback(error) if error? plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + if !plan? + err = new Error("No plan found for planCode '#{subscription.planCode}'") + logger.error {user_id: user._id, err}, "error getting subscription plan for user" + return callback(err) RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)-> tax = recurlySubscription?.tax_in_cents || 0 callback null, { diff --git a/services/web/app/coffee/Features/Tags/TagsController.coffee b/services/web/app/coffee/Features/Tags/TagsController.coffee index 2e67be2fd4..0cd15ab5e7 100644 --- a/services/web/app/coffee/Features/Tags/TagsController.coffee +++ b/services/web/app/coffee/Features/Tags/TagsController.coffee @@ -1,48 +1,49 @@ TagsHandler = require("./TagsHandler") logger = require("logger-sharelatex") +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = getAllTags: (req, res, next)-> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) logger.log {user_id}, "getting tags" TagsHandler.getAllTags user_id, (error, allTags)-> return next(error) if error? res.json(allTags) - + createTag: (req, res, next) -> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) name = req.body.name logger.log {user_id, name}, "creating tag" TagsHandler.createTag user_id, name, (error, tag) -> return next(error) if error? res.json(tag) - + addProjectToTag: (req, res, next) -> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) {tag_id, project_id} = req.params logger.log {user_id, tag_id, project_id}, "adding tag to project" TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) -> return next(error) if error? res.status(204).end() - + removeProjectFromTag: (req, res, next) -> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) {tag_id, project_id} = req.params logger.log {user_id, tag_id, project_id}, "removing tag from project" TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) -> return next(error) if error? res.status(204).end() - + deleteTag: (req, res, next) -> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) tag_id = req.params.tag_id logger.log {user_id, tag_id}, "deleting tag" TagsHandler.deleteTag user_id, tag_id, (error) -> return next(error) if error? res.status(204).end() - + renameTag: (req, res, next) -> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) tag_id = req.params.tag_id name = req.body?.name if !name? diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index 212c251029..5e20c2b515 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -33,16 +33,11 @@ module.exports = self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback deleteUpdate: (project_id, path, source, callback)-> - projectLocator.findElementByPath project_id, path, (err, element)-> - type = 'file' + projectLocator.findElementByPath project_id, path, (err, element, type)-> if err? || !element? logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted" return callback() - if element.lines? - type = 'doc' - else if element.folders? - type = 'folder' - logger.log project_id:project_id, updateType:path, updateType:type, element:element, "processing update to delete entity from tpds" + logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds" editorController.deleteEntity project_id, element._id, type, source, (err)-> logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" callback() @@ -56,12 +51,13 @@ module.exports = return callback(err) logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds" if doc_id? - editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)-> - callback() + editorController.setDoc project_id, doc_id, user_id, docLines, source, callback else setupNewEntity project_id, path, (err, folder, fileName)-> - editorController.addDoc project_id, folder._id, fileName, docLines, source, (err)-> - callback() + if err? + logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file" + return callback(err) + editorController.addDoc project_id, folder._id, fileName, docLines, source, callback processFile: (project_id, file_id, fsPath, path, source, callback)-> finish = (err)-> @@ -69,10 +65,13 @@ module.exports = callback(err) logger.log project_id:project_id, file_id:file_id, path:path, "processing file update from tpds" setupNewEntity project_id, path, (err, folder, fileName) => - if file_id? + if err? + logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file" + return callback(err) + else if file_id? editorController.replaceFile project_id, file_id, fsPath, source, finish else - editorController.addFile project_id, folder._id, fileName, fsPath, source, finish + editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)-> if !file_id? diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee index f548cadde6..bc6e00a29a 100644 --- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee +++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee @@ -5,17 +5,16 @@ AuthenticationController = require "../Authentication/AuthenticationController" module.exports = TrackChangesController = proxyToTrackChangesApi: (req, res, next = (error) ->) -> - AuthenticationController.getLoggedInUserId req, (error, user_id) -> - return next(error) if error? - url = settings.apis.trackchanges.url + req.url - logger.log url: url, "proxying to track-changes api" - getReq = request( - url: url - method: req.method - headers: - "X-User-Id": user_id - ) - getReq.pipe(res) - getReq.on "error", (error) -> - logger.error err: error, "track-changes API error" - next(error) \ No newline at end of file + user_id = AuthenticationController.getLoggedInUserId req + url = settings.apis.trackchanges.url + req.url + logger.log url: url, "proxying to track-changes api" + getReq = request( + url: url + method: req.method + headers: + "X-User-Id": user_id + ) + getReq.pipe(res) + getReq.on "error", (error) -> + logger.error err: error, "track-changes API error" + next(error) diff --git a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee index b7cab7ac5c..de23c45015 100644 --- a/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee +++ b/services/web/app/coffee/Features/Uploads/ProjectUploadController.coffee @@ -4,11 +4,12 @@ fs = require "fs" Path = require "path" FileSystemImportManager = require "./FileSystemImportManager" ProjectUploadManager = require "./ProjectUploadManager" +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = ProjectUploadController = uploadProject: (req, res, next) -> timer = new metrics.Timer("project-upload") - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) {originalname, path} = req.files.qqfile name = Path.basename(originalname, ".zip") ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) -> @@ -24,18 +25,18 @@ module.exports = ProjectUploadController = project: project._id, file_path: path, file_name: name, "uploaded project" res.send success: true, project_id: project._id - + uploadFile: (req, res, next) -> timer = new metrics.Timer("file-upload") - name = req.files.qqfile.originalname - path = req.files.qqfile.path + name = req.files.qqfile?.originalname + path = req.files.qqfile?.path project_id = req.params.Project_id folder_id = req.query.folder_id if !name? or name.length == 0 or name.length > 150 logger.err project_id:project_id, name:name, "bad name when trying to upload file" return res.send success: false logger.log folder_id:folder_id, project_id:project_id, "getting upload file request" - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) -> fs.unlink path, -> timer.done() @@ -50,6 +51,3 @@ module.exports = ProjectUploadController = project_id: project_id, file_path: path, file_name: name, folder_id: folder_id "uploaded file" res.send success: true, entity_id: entity?._id - - - diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 546cea035e..389de1a0f2 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -8,6 +8,7 @@ logger = require("logger-sharelatex") metrics = require("../../infrastructure/Metrics") Url = require("url") AuthenticationManager = require("../Authentication/AuthenticationManager") +AuthenticationController = require('../Authentication/AuthenticationController') UserSessionsManager = require("./UserSessionsManager") UserUpdater = require("./UserUpdater") settings = require "settings-sharelatex" @@ -15,20 +16,21 @@ settings = require "settings-sharelatex" module.exports = UserController = deleteUser: (req, res)-> - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) UserDeleter.deleteUser user_id, (err)-> if !err? req.session?.destroy() res.sendStatus(200) unsubscribe: (req, res)-> - UserLocator.findById req.session.user._id, (err, user)-> + user_id = AuthenticationController.getLoggedInUserId(req) + UserLocator.findById user_id, (err, user)-> newsLetterManager.unsubscribe user, -> res.send() updateUserSettings : (req, res)-> - logger.log user: req.session.user, "updating account settings" - user_id = req.session.user._id + user_id = AuthenticationController.getLoggedInUserId(req) + logger.log user_id: user_id, "updating account settings" User.findById user_id, (err, user)-> if err? or !user? logger.err err:err, user_id:user_id, "problem updaing user settings" @@ -54,9 +56,12 @@ module.exports = UserController = user.ace.spellCheckLanguage = req.body.spellCheckLanguage if req.body.pdfViewer? user.ace.pdfViewer = req.body.pdfViewer + if req.body.syntaxValidation? + user.ace.syntaxValidation = req.body.syntaxValidation user.save (err)-> newEmail = req.body.email?.trim().toLowerCase() if !newEmail? or newEmail == user.email + AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name}) return res.sendStatus 200 else if newEmail.indexOf("@") == -1 return res.sendStatus(400) @@ -73,7 +78,7 @@ module.exports = UserController = if err? logger.err err:err, user_id:user_id, "error getting user for email update" return res.send 500 - req.session.user.email = user.email + AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name}) UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background if err? logger.err err:err, "error populateGroupLicenceInvite" @@ -81,9 +86,10 @@ module.exports = UserController = logout : (req, res)-> metrics.inc "user.logout" - logger.log user: req?.session?.user, "logging out" + user = AuthenticationController.getSessionUser(req) + logger.log user: user, "logging out" sessionId = req.sessionID - user = req?.session?.user + req.logout?() # passport logout req.session.destroy (err)-> if err logger.err err: err, 'error destorying session' @@ -102,13 +108,22 @@ module.exports = UserController = setNewPasswordUrl: setNewPasswordUrl } + clearSessions: (req, res, next = (error) ->) -> + metrics.inc "user.clear-sessions" + user = AuthenticationController.getSessionUser(req) + logger.log {user_id: user._id}, "clearing sessions for user" + UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) -> + return next(err) if err? + res.sendStatus 201 + changePassword : (req, res, next = (error) ->)-> metrics.inc "user.password-change" oldPass = req.body.currentPassword - AuthenticationManager.authenticate {_id:req.session.user._id}, oldPass, (err, user)-> + user_id = AuthenticationController.getLoggedInUserId(req) + AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)-> return next(err) if err? if(user) - logger.log user: req.session.user, "changing password" + logger.log user: user._id, "changing password" newPassword1 = req.body.newPassword1 newPassword2 = req.body.newPassword2 if newPassword1 != newPassword2 diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index edaf836c80..92f111bda7 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -3,12 +3,14 @@ logger = require("logger-sharelatex") UserDeleter = require("./UserDeleter") UserUpdater = require("./UserUpdater") sanitize = require('sanitizer') +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = UserController = getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) -> - logger.log user: req.user, "reciving request for getting logged in users personal info" - return next(new Error("User is not logged in")) if !req.user? - UserGetter.getUser req.user._id, { + user_id = AuthenticationController.getLoggedInUserId(req) + logger.log user_id: user_id, "reciving request for getting logged in users personal info" + return next(new Error("User is not logged in")) if !user_id? + UserGetter.getUser user_id, { first_name: true, last_name: true, role:true, institution:true, email: true, signUpDate: true @@ -38,6 +40,3 @@ module.exports = UserController = role: user.role institution: user.institution } - - - diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 567cacd35c..76d88803a7 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -1,9 +1,11 @@ UserLocator = require("./UserLocator") UserGetter = require("./UserGetter") +UserSessionsManager = require("./UserSessionsManager") ErrorController = require("../Errors/ErrorController") logger = require("logger-sharelatex") Settings = require("settings-sharelatex") fs = require('fs') +AuthenticationController = require('../Authentication/AuthenticationController') module.exports = @@ -22,14 +24,14 @@ module.exports = sharedProjectData: sharedProjectData newTemplateData: newTemplateData new_email:req.query.new_email || "" - + activateAccountPage: (req, res) -> # An 'activation' is actually just a password reset on an account that # was set with a random password originally. logger.log query:req.query, "activiate account page called" if !req.query?.user_id? or !req.query?.token? return ErrorController.notFound(req, res) - + UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) -> return next(error) if error? if !user @@ -53,11 +55,23 @@ module.exports = email: req.query.email settingsPage : (req, res, next)-> - logger.log user: req.session.user, "loading settings page" - UserLocator.findById req.session.user._id, (err, user)-> + user_id = AuthenticationController.getLoggedInUserId(req) + logger.log user: user_id, "loading settings page" + UserLocator.findById user_id, (err, user)-> return next(err) if err? res.render 'user/settings', title:'account_settings' user: user, languages: Settings.languages, accountSettingsTabActive: true + + sessionsPage: (req, res, next) -> + user = AuthenticationController.getSessionUser(req) + logger.log user_id: user._id, "loading sessions page" + UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) -> + if err? + logger.err {user_id: user._id}, "error getting all user sessions" + return next(err) + res.render 'user/sessions', + title: "sessions" + sessions: sessions diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index 75a9debba4..f5db2e54a1 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -8,6 +8,7 @@ logger = require("logger-sharelatex") crypto = require("crypto") EmailHandler = require("../Email/EmailHandler") OneTimeTokenHandler = require "../Security/OneTimeTokenHandler" +Analytics = require "../Analytics/AnalyticsManager" settings = require "settings-sharelatex" module.exports = UserRegistrationHandler = @@ -62,6 +63,7 @@ module.exports = UserRegistrationHandler = cb() #this can be slow, just fire it off ], (err)-> logger.log user: user, "registered" + Analytics.recordEvent user._id, "user-registered" callback(err, user) registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) -> diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee index 95974ec59a..23a9bcbb36 100644 --- a/services/web/app/coffee/Features/User/UserSessionsManager.coffee +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -55,11 +55,40 @@ module.exports = UserSessionsManager = UserSessionsManager._checkSessions(user, () ->) callback() + getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) -> + exclude = _.map(exclude, UserSessionsManager._sessionKey) + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err? + logger.err user_id: user._id, "error getting all session keys for user from redis" + return callback(err) + sessionKeys = _.filter sessionKeys, (k) -> !(_.contains(exclude, k)) + if sessionKeys.length == 0 + logger.log {user_id: user._id}, "no other sessions found, returning" + return callback(null, []) + rclient.mget sessionKeys, (err, sessions) -> + if err? + logger.err {user_id: user._id}, "error getting all sessions for user from redis" + return callback(err) + + result = [] + for session in sessions + if session is null + continue + session = JSON.parse(session) + session_user = session?.user or session?.passport?.user + result.push { + ip_address: session_user.ip_address, + session_created: session_user.session_created + } + + return callback(null, result) + revokeAllUserSessions: (user, retain, callback=(err)->) -> - if !retain + if !retain? retain = [] retain = retain.map((i) -> UserSessionsManager._sessionKey(i)) - if !user + if !user? logger.log {}, "no user to revoke sessions for, returning" return callback(null) logger.log {user_id: user._id}, "revoking all existing sessions for user" diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index ee7418dacc..8763624516 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -5,42 +5,54 @@ Settings = require('settings-sharelatex') SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters') querystring = require('querystring') SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager") +AuthenticationController = require("../Features/Authentication/AuthenticationController") _ = require("underscore") +async = require("async") Modules = require "./Modules" Url = require "url" - +PackageVersions = require "./PackageVersions" fingerprints = {} Path = require 'path' - jsPath = if Settings.useMinifiedJs "/minjs/" else "/js/" +ace = PackageVersions.lib('ace') +pdfjs = PackageVersions.lib('pdfjs') -logger.log "Generating file fingerprints..." -for path in [ - "#{jsPath}libs/require.js", - "#{jsPath}ide.js", - "#{jsPath}main.js", - "#{jsPath}libs.js", - "#{jsPath}ace/ace.js", - "#{jsPath}libs/pdfjs-1.3.91/pdf.js", - "#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js", - "#{jsPath}libs/pdfjs-1.3.91/compatibility.js", - "/stylesheets/style.css" -] - filePath = Path.join __dirname, "../../../", "public#{path}" +getFileContent = (filePath)-> + filePath = Path.join __dirname, "../../../", "public#{filePath}" exists = fs.existsSync filePath if exists content = fs.readFileSync filePath - hash = crypto.createHash("md5").update(content).digest("hex") - logger.log "#{filePath}: #{hash}" - fingerprints[path] = hash + return content else logger.log filePath:filePath, "file does not exist for fingerprints" + return "" + +logger.log "Generating file fingerprints..." +pathList = [ + ["#{jsPath}libs/require.js"] + ["#{jsPath}ide.js"] + ["#{jsPath}main.js"] + ["#{jsPath}libs.js"] + ["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js", "#{jsPath}#{ace}/snippets/latex.js"] + ["#{jsPath}libs/#{pdfjs}/pdf.js"] + ["#{jsPath}libs/#{pdfjs}/pdf.worker.js"] + ["#{jsPath}libs/#{pdfjs}/compatibility.js"] + ["/stylesheets/style.css"] +] + +for paths in pathList + contentList = _.map(paths, getFileContent) + content = contentList.join("") + hash = crypto.createHash("md5").update(content).digest("hex") + _.each paths, (filePath)-> + logger.log "#{filePath}: #{hash}" + fingerprints[filePath] = hash getFingerprint = (path) -> if fingerprints[path]? @@ -59,28 +71,35 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.session = req.session next() - webRouter.use (req, res, next)-> + webRouter.use (req, res, next)-> + + cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked + user_id = AuthenticationController.getLoggedInUserId(req) + + if cdnBlocked and !req.session.cdnBlocked? + logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets" + req.session.cdnBlocked = true isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark" isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke" isLive = !isDark and !isSmoke - if cdnAvailable and isLive + if cdnAvailable and isLive and !cdnBlocked staticFilesBase = Settings.cdn?.web?.host else if darkCdnAvailable and isDark staticFilesBase = Settings.cdn?.web?.darkHost else staticFilesBase = "" - + res.locals.jsPath = jsPath res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath) - + res.locals.lib = PackageVersions.lib res.locals.buildJsPath = (jsFile, opts = {})-> path = Path.join(jsPath, jsFile) doFingerPrint = opts.fingerprint != false - + if !opts.qs? opts.qs = {} @@ -89,14 +108,13 @@ module.exports = (app, webRouter, apiRouter)-> if opts.cdn != false path = Url.resolve(staticFilesBase, path) - + qs = querystring.stringify(opts.qs) if qs? and qs.length > 0 path = path + "?" + qs return path - res.locals.buildCssPath = (cssFile)-> path = Path.join("/stylesheets/", cssFile) return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path) @@ -109,7 +127,7 @@ module.exports = (app, webRouter, apiRouter)-> - webRouter.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.settings = Settings next() @@ -117,7 +135,9 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.translate = (key, vars = {}) -> vars.appName = Settings.appName req.i18n.translate(key, vars) - res.locals.currentUrl = req.originalUrl + # Don't include the query string parameters, otherwise Google + # treats ?nocdn=true as the canonical version + res.locals.currentUrl = Url.parse(req.originalUrl).pathname next() webRouter.use (req, res, next)-> @@ -125,9 +145,10 @@ module.exports = (app, webRouter, apiRouter)-> Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2) next() - webRouter.use (req, res, next)-> + webRouter.use (req, res, next) -> res.locals.getUserEmail = -> - email = req?.session?.user?.email or "" + user = AuthenticationController.getSessionUser(req) + email = user?.email or "" return email next() @@ -137,15 +158,17 @@ module.exports = (app, webRouter, apiRouter)-> return formatedPrivileges[privilegeLevel] || "Private" next() - webRouter.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.buildReferalUrl = (referal_medium) -> url = Settings.siteUrl - if req.session? and req.session.user? and req.session.user.referal_id? - url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus + currentUser = AuthenticationController.getSessionUser(req) + if currentUser? and currentUser?.referal_id? + url+="?r=#{currentUser.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus return url res.locals.getReferalId = -> - if req.session? and req.session.user? and req.session.user.referal_id - return req.session.user.referal_id + currentUser = AuthenticationController.getSessionUser(req) + if currentUser? and currentUser?.referal_id? + return currentUser.referal_id res.locals.getReferalTagLine = -> tagLines = [ "Roar!" @@ -161,7 +184,11 @@ module.exports = (app, webRouter, apiRouter)-> return "" res.locals.getLoggedInUserId = -> - return req.session.user?._id + return AuthenticationController.getLoggedInUserId(req) + res.locals.isUserLoggedIn = -> + return AuthenticationController.isUserLoggedIn(req) + res.locals.getSessionUser = -> + return AuthenticationController.getSessionUser(req) next() webRouter.use (req, res, next) -> @@ -173,11 +200,11 @@ module.exports = (app, webRouter, apiRouter)-> return req.query?[field] next() - webRouter.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.fingerprint = getFingerprint next() - webRouter.use (req, res, next)-> + webRouter.use (req, res, next)-> res.locals.formatPrice = SubscriptionFormatters.formatPrice next() @@ -187,11 +214,12 @@ module.exports = (app, webRouter, apiRouter)-> next() webRouter.use (req, res, next)-> - if req.session.user? + currentUser = AuthenticationController.getSessionUser(req) + if currentUser? res.locals.user = - email: req.session.user.email - first_name: req.session.user.first_name - last_name: req.session.user.last_name + email: currentUser.email + first_name: currentUser.first_name + last_name: currentUser.last_name if req.session.justRegistered res.locals.justRegistered = true delete req.session.justRegistered @@ -217,7 +245,7 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.nav[key] = _.clone(Settings.nav[key]) res.locals.templates = Settings.templateLinks next() - + webRouter.use (req, res, next) -> SystemMessageManager.getMessages (error, messages = []) -> res.locals.systemMessages = messages @@ -240,5 +268,3 @@ module.exports = (app, webRouter, apiRouter)-> res.locals.moduleIncludes = Modules.moduleIncludes res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable next() - - diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee index 5b64b8e6f4..88f3ed4bfa 100644 --- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee +++ b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee @@ -2,6 +2,7 @@ request = require("request") settings = require("settings-sharelatex") _ = require("underscore") logger = require("logger-sharelatex") +URL = require("url") currencyMappings = { "GB":"GBP" @@ -31,7 +32,7 @@ module.exports = GeoIpLookup = return callback(e) ip = ip.trim().split(" ")[0] opts = - url: "#{settings.apis.geoIpLookup.url}/#{ip}" + url: URL.resolve(settings.apis.geoIpLookup.url,ip) timeout: 1000 json:true logger.log ip:ip, opts:opts, "getting geo ip details" diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee index 2df8907f7e..0dfbf3fa22 100644 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ b/services/web/app/coffee/infrastructure/Modules.coffee @@ -25,14 +25,14 @@ module.exports = Modules = for module in @modules for view, partial of module.viewIncludes or {} @viewIncludes[view] ||= [] - @viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")) + @viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html") moduleIncludes: (view, locals) -> - partials = Modules.viewIncludes[view] or [] + compiledPartials = Modules.viewIncludes[view] or [] html = "" - for partial in partials - compiler = jade.compile(partial, doctype: "html") - html += compiler(locals) + for compiledPartial in compiledPartials + d = new Date() + html += compiledPartial(locals) return html moduleIncludesAvailable: (view) -> diff --git a/services/web/app/coffee/infrastructure/PackageVersions.coffee b/services/web/app/coffee/infrastructure/PackageVersions.coffee new file mode 100644 index 0000000000..53c31e4345 --- /dev/null +++ b/services/web/app/coffee/infrastructure/PackageVersions.coffee @@ -0,0 +1,15 @@ +version = { + "pdfjs": "1.6.210p1" + "moment": "2.9.0" + "ace": "1.2.5" +} + +module.exports = { + version: version + + lib: (name) -> + if version[name]? + return "#{name}-#{version[name]}" + else + return "#{name}" +} diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 8ac543c698..7d551f89b3 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -21,6 +21,9 @@ cookieParser = require('cookie-parser') sessionStore = new RedisStore(client:rclient) +passport = require('passport') +LocalStrategy = require('passport-local').Strategy + Mongoose = require("./Mongoose") oneDayInMilliseconds = 86400000 @@ -32,6 +35,7 @@ Modules = require "./Modules" ErrorController = require "../Features/Errors/ErrorController" UserSessionsManager = require "../Features/User/UserSessionsManager" +AuthenticationController = require "../Features/Authentication/AuthenticationController" metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger) @@ -87,11 +91,26 @@ webRouter.use csrfProtection webRouter.use translations.expressMiddlewear webRouter.use translations.setLangBasedOnDomainMiddlewear +# passport +webRouter.use passport.initialize() +webRouter.use passport.session() + +passport.use(new LocalStrategy( + { + passReqToCallback: true, + usernameField: 'email', + passwordField: 'password' + }, + AuthenticationController.doPassportLogin +)) +passport.serializeUser(AuthenticationController.serializeUser) +passport.deserializeUser(AuthenticationController.deserializeUser) + # Measure expiry from last request, not last login webRouter.use (req, res, next) -> req.session.touch() - if req?.session?.user? - UserSessionsManager.touch(req.session.user, (err)->) + if AuthenticationController.isUserLoggedIn(req) + UserSessionsManager.touch(AuthenticationController.getSessionUser(req), (err)->) next() webRouter.use ReferalConnect.use diff --git a/services/web/app/coffee/models/ProjectInvite.coffee b/services/web/app/coffee/models/ProjectInvite.coffee index 3349dafa9b..9b9e0cb350 100644 --- a/services/web/app/coffee/models/ProjectInvite.coffee +++ b/services/web/app/coffee/models/ProjectInvite.coffee @@ -1,23 +1,42 @@ mongoose = require 'mongoose' Settings = require 'settings-sharelatex' + Schema = mongoose.Schema ObjectId = Schema.ObjectId -ProjectInviteSchema = new Schema - project_id: ObjectId - from_user_id: ObjectId - privilegeLevel: String - # For existing users - to_user_id: ObjectId - # For non-existant users - hashed_token: String - email: String + +EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30 + +ExpiryDate = () -> + timestamp = new Date() + timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS) + return timestamp + + + +ProjectInviteSchema = new Schema( + { + email: String + token: String + sendingUserId: ObjectId + projectId: ObjectId + privileges: String + createdAt: {type: Date, default: Date.now} + expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}} + }, + { + collection: 'projectInvites' + } +) + conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10) + ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema) mongoose.model 'ProjectInvite', ProjectInviteSchema exports.ProjectInvite = ProjectInvite -exports.ProjectInviteSchema = ProjectInviteSchema \ No newline at end of file +exports.ProjectInviteSchema = ProjectInviteSchema +exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index ba0a862fd2..3b29936ab2 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -26,6 +26,7 @@ UserSchema = new Schema autoComplete: {type : Boolean, default: true} spellCheckLanguage : {type : String, default: "en"} pdfViewer : {type : String, default: "pdfjs"} + syntaxValidation : {type : Boolean, default: true} } features : { collaborators: { type:Number, default: Settings.defaultFeatures.collaborators } diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index d6deffe755..56dd8d821b 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -11,7 +11,6 @@ SubscriptionRouter = require './Features/Subscription/SubscriptionRouter' UploadsRouter = require './Features/Uploads/UploadsRouter' metrics = require('./infrastructure/Metrics') ReferalController = require('./Features/Referal/ReferalController') -ReferalMiddleware = require('./Features/Referal/ReferalMiddleware') AuthenticationController = require('./Features/Authentication/AuthenticationController') TagsController = require("./Features/Tags/TagsController") NotificationsController = require("./Features/Notifications/NotificationsController") @@ -22,6 +21,7 @@ UserPagesController = require('./Features/User/UserPagesController') DocumentController = require('./Features/Documents/DocumentController') CompileManager = require("./Features/Compile/CompileManager") CompileController = require("./Features/Compile/CompileController") +ClsiCookieManager = require("./Features/Compile/ClsiCookieManager") HealthCheckController = require("./Features/HealthCheck/HealthCheckController") ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" FileStoreController = require("./Features/FileStore/FileStoreController") @@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') BetaProgramController = require('./Features/BetaProgram/BetaProgramController') +AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') logger = require("logger-sharelatex") _ = require("underscore") @@ -51,7 +52,8 @@ module.exports = class Router webRouter.get '/login', UserPagesController.loginPage AuthenticationController.addEndpointToLoginWhitelist '/login' - webRouter.post '/login', AuthenticationController.login + webRouter.post '/login', AuthenticationController.passportLogin + webRouter.get '/logout', UserController.logout webRouter.get '/restricted', AuthorizationMiddlewear.restricted @@ -68,12 +70,13 @@ module.exports = class Router StaticPagesRouter.apply(webRouter, apiRouter) RealTimeProxyRouter.apply(webRouter, apiRouter) ContactRouter.apply(webRouter, apiRouter) + AnalyticsRouter.apply(webRouter, apiRouter) Modules.applyRouter(webRouter, apiRouter) if Settings.enableSubscriptions - webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus + webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus webRouter.get '/blog', BlogController.getIndexPage webRouter.get '/blog/*', BlogController.getPage @@ -85,6 +88,9 @@ module.exports = class Router webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword + webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage + webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions + webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser @@ -179,7 +185,7 @@ module.exports = class Router webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications - webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead + webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead # Deprecated in favour of /internal/project/:project_id but still used by versioning apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails @@ -249,14 +255,27 @@ module.exports = class Router apiRouter.get '/health_check/redis', HealthCheckController.checkRedis apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> + project_id = req.params.Project_id sendRes = _.once (statusCode, message)-> - res.writeHead statusCode - res.end message - CompileManager.compile req.params.Project_id, "test-compile", {}, () -> - sendRes 200, "Compiler returned in less than 10 seconds" - setTimeout (() -> + res.status statusCode + res.send message + ClsiCookieManager.clearServerId project_id # force every compile to a new server + # set a timeout + handler = setTimeout (() -> sendRes 500, "Compiler timed out" + handler = null ), 10000 + # use a valid user id for testing + test_user_id = "123456789012345678901234" + # run the compile + CompileManager.compile project_id, test_user_id, {}, (error, status) -> + clearTimeout handler if handler? + if error? + sendRes 500, "Compiler returned error #{error.message}" + else if status is "success" + sendRes 200, "Compiler returned in less than 10 seconds" + else + sendRes 500, "Compiler returned failure #{status}" apiRouter.get "/ip", (req, res, next) -> res.send({ @@ -280,4 +299,4 @@ module.exports = class Router metrics.inc("client-side-error") res.sendStatus(204) - webRouter.get '*', ErrorController.notFound \ No newline at end of file + webRouter.get '*', ErrorController.notFound diff --git a/services/web/app/views/beta_program/opt_in.jade b/services/web/app/views/beta_program/opt_in.jade index c6411cb211..f54766f30d 100644 --- a/services/web/app/views/beta_program/opt_in.jade +++ b/services/web/app/views/beta_program/opt_in.jade @@ -22,7 +22,7 @@ block content ul.list-unstyled.text-center li i.fa.fa-fw.fa-book - | #{translate("mendeley_integration")} + | #{translate("syntax_checking")} .row.text-centered .col-md-12 if user.betaProgram diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 83d7da4838..7f5cfd891f 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product') window.similarproducts = true style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; } + -if (typeof(gaExperiments) != "undefined") |!{gaExperiments} @@ -46,83 +47,22 @@ html(itemscope, itemtype='http://schema.org/Product') - else script(type='text/javascript'). window.ga = function() { console.log("would send to GA", arguments) }; - - // Countly Analytics - if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token) - script(type="text/javascript"). - var Countly = Countly || {}; - Countly.q = Countly.q || []; - Countly.app_key = '#{settings.analytics.countly.token}'; - Countly.url = '#{settings.analytics.countly.server}'; - !{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' } - - (function() { - var cly = document.createElement('script'); cly.type = 'text/javascript'; - cly.async = true; - //enter url of script here - cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js'; - cly.onload = function(){Countly.init()}; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s); - })(); - - script(type="text/javascript") - if (session && session.user) - - var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : ''); - | Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]); - - if (justRegistered) - | Countly.q.push(['add_event',{ key: 'user-registered' }]); - - if (justLoggedIn) - | Countly.q.push(['add_event',{ key: 'user-logged-in' }]); - - if (user && user.features) - - featureFlagSet = false; - - if user.features.hasOwnProperty('collaborators') - | Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('compileGroup') - | Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('compileTimeout') - | Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('dropbox') - | Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('github') - | Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('references') - | Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('templates') - | Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]); - - featureFlagSet = true; - - if user.features.hasOwnProperty('versioning') - | Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]); - - featureFlagSet = true; - - - if featureFlagSet - | Countly.q.push(['userData.save']) - - // End countly Analytics script(type="text/javascript"). window.csrfToken = "#{csrfToken}"; block scripts + script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false})) + script(type="text/javascript"). + var noCdnKey = "nocdn=true" + var cdnBlocked = typeof jQuery === 'undefined' + var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops + if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) { + window.location.search += '&'+noCdnKey; + } script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false})) + script. window.sharelatex = { siteUrl: '#{settings.siteUrl}', @@ -171,23 +111,27 @@ html(itemscope, itemtype='http://schema.org/Product') include layout/navbar block content + div(ng-controller="AbTestController") - if(typeof(suppressFooter) == "undefined") include layout/footer - - - if (typeof(lookingForScribtex) != "undefined" && lookingForScribtex) span(ng-controller="ScribtexPopupController") include scribtex-modal - - - if(typeof(suppressFooter) == "undefined") + block requirejs script(type='text/javascript'). + // minimal requirejs configuration (can be extended/overridden) window.requirejs = { - "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "paths" : { - "moment": "libs/moment-2.7.0" + "moment": "libs/#{lib('moment')}" + }, + "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", + "config":{ + "moment":{ + "noGlobal": true + } } }; script( @@ -196,7 +140,6 @@ html(itemscope, itemtype='http://schema.org/Product') src=buildJsPath('libs/require.js') ) - include contact-us-modal include sentry diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index b6b3c61741..e0a89fdea7 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -13,7 +13,7 @@ nav.navbar.navbar-default .navbar-collapse.collapse(collapse="navCollapsed") ul.nav.navbar-nav.navbar-right - if (session && session.user && session.user.isAdmin) + if (getSessionUser() && getSessionUser().isAdmin) li.dropdown(class="subdued", dropdown) a.dropdown-toggle(href, dropdown-toggle) | Admin @@ -25,7 +25,7 @@ nav.navbar.navbar-default a(href="/admin/user") Manage Users each item in nav.header - if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in)) + if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in)) if item.dropdown li.dropdown(class=item.class, dropdown) a.dropdown-toggle(href, dropdown-toggle) diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 1e36e374f1..ff6e5ef2a0 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -3,7 +3,6 @@ extends ../layout block vars - var suppressNavbar = true - var suppressFooter = true - - var suppressDefaultJs = true - var suppressSystemMessages = true block content @@ -66,7 +65,7 @@ block content .ui-layout-center include ./editor/editor include ./editor/binary-file - include ./editor/track-changes + include ./editor/history include ./editor/publish-template .ui-layout-east @@ -86,7 +85,11 @@ block content .modal-footer button.btn.btn-info(ng-click="done()") #{translate("ok")} - script(src='/socket.io/socket.io.js') +block requirejs + script(type="text/javascript" src='/socket.io/socket.io.js') + + //- don't use cdn for worker + - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false}) //- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>' //- and doesn't prematurely end the script tag. @@ -100,47 +103,43 @@ block content window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { - "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}", - "moment": "libs/moment-2.7.0", - "libs/pdf": "libs/pdfjs-1.3.91/pdf" + "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}", + "moment": "libs/#{lib('moment')}", + "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", + "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", + "ace": "#{lib('ace')}" }, "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", "waitSeconds": 0, "shim": { - "libs/pdf": { - deps: ["libs/pdfjs-1.3.91/compatibility"] + "pdfjs-dist/build/pdf": { + "deps": ["libs/#{lib('pdfjs')}/compatibility"] }, "ace/ext-searchbox": { - deps: ["ace/ace"] + "deps": ["ace/ace"] }, "ace/ext-language_tools": { - deps: ["ace/ace"] + "deps": ["ace/ace"] } }, - config:{ - moment:{ - noGlobal: true + "config":{ + "moment":{ + "noGlobal": true } } }; - window.aceFingerprint = "#{fingerprint(jsPath + 'ace/ace.js')}" - - - locals.suppressDefaultJs = true - - - var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js' - - var fingerprintedPath = fingerprint(jsPath+pdfPath) - - var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn + window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}" + - var aceWorkerPath = user.betaProgram ? buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) : "" // don't use cdn for worker script(type='text/javascript'). - window.pdfJsWorkerPath = "#{pdfJsWorkerPath}"; + window.aceWorkerPath = "#{aceWorkerPath}"; script( data-main=buildJsPath("ide.js", {fingerprint:false}), baseurl=fullJsPath, - data-ace-base=buildJsPath('ace', {fingerprint:false}), + data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), src=buildJsPath('libs/require.js') ) - - + \ No newline at end of file diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade index 538a5b9d37..47a1752834 100644 --- a/services/web/app/views/project/editor/chat.jade +++ b/services/web/app/views/project/editor/chat.jade @@ -41,11 +41,12 @@ aside.chat( }" ) .arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}") - p( - mathjax, - ng-repeat="content in message.contents track by $index" - ) - span(ng-bind-html="content | linky:'_blank'") + .message-content + p( + mathjax, + ng-repeat="content in message.contents track by $index" + ) + span(ng-bind-html="content | linky:'_blank'") .new-message textarea( diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index ea5efff304..07208a6642 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -36,6 +36,7 @@ div.full-size( annotations="pdf.logEntryAnnotations[editor.open_doc_id]", read-only="!permissions.write", on-ctrl-enter="recompileViaKey" + syntax-validation="settings.syntaxValidation" ) .ui-layout-east diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index 77cbd10da9..92af8b627d 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']" ) - li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'") + li(ng-show="deletedDocs.length > 0 && ui.view == 'history'") h3 #{translate("deleted_files")} li( ng-class="{ 'selected': entity.selected }", ng-repeat="entity in deletedDocs | orderBy:'name'", ng-controller="FileTreeEntityController", - ng-show="ui.view == 'track-changes'" + ng-show="ui.view == 'history'" ) .entity .entity-name( @@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate') required, ng-model="inputs.name", on-enter="create()", - select-name-on="open" + select-name-on="open", + ng-pattern="validFileRegex", + name="name" ) + .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern") + | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" @@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate') required, ng-model="inputs.name", on-enter="create()", - select-name-on="open" + select-name-on="open", + ng-pattern="validFileRegex", + name="name" ) + .text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern") + | #{translate('files_cannot_include_invalid_characters')} .modal-footer button.btn.btn-default( ng-disabled="state.inflight" @@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate') ) span(ng-hide="state.inflight") #{translate("delete")} span(ng-show="state.inflight") #{translate("deleting")}... + +script(type='text/ng-template', id='invalidFileNameModalTemplate') + .modal-header + h3 #{translate('invalid_file_name')} + .modal-body + p #{translate('files_cannot_include_invalid_characters')} + .modal-footer + button.btn.btn-default( + ng-click="$close()" + ) #{translate('ok')} \ No newline at end of file diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade index 06ac949c04..02cec9d2ca 100644 --- a/services/web/app/views/project/editor/header.jade +++ b/services/web/app/views/project/editor/header.jade @@ -1,120 +1,380 @@ -header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading") - a.btn.btn-full-height( - href, - ng-click="ui.leftMenuShown = true" - tooltip='#{translate("menu")}', - tooltip-placement="bottom", - tooltip-append-to-body="true" +div(ng-if="!shouldABTestHeaderLabels") + header.toolbar.toolbar-header( + ng-cloak, + ng-hide="state.loading" ) - i.fa.fa-fw.fa-bars - a( - href="/project" - tooltip="#{translate('back_to_projects')}", - tooltip-placement="bottom", - tooltip-append-to-body="true" - ) - i.fa.fa-fw.fa-level-up - span(ng-controller="PdfViewToggleController") + a.btn.btn-full-height( + href, + ng-click="ui.leftMenuShown = true" + tooltip='#{translate("menu")}', + tooltip-placement="bottom", + tooltip-append-to-body="true", + ) + i.fa.fa-fw.fa-bars a( - href, - ng-show="ui.pdfLayout == 'flat' && fileTreeClosed", - tooltip="PDF", + href="/project" + tooltip="#{translate('back_to_projects')}", tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="togglePdfView()", - ng-class="{ 'active': ui.view == 'pdf' }" + tooltip-append-to-body="true" ) - i.fa.fa-file-pdf-o - - .toolbar-center.project-name(ng-controller="ProjectNameController") - span.name( - ng-dblclick="!permissions.admin || startRenaming()", - ng-show="!state.renaming" - ) {{ project.name }} - - input.form-control( - type="text" - ng-model="inputs.name", - ng-show="state.renaming", - on-enter="finishRenaming()", - ng-blur="finishRenaming()", - select-name-when="state.renaming" - ) - - a.rename( - ng-if="permissions.admin", - href='#', - tooltip-placement="bottom", - tooltip="#{translate('rename')}", - tooltip-append-to-body="true", - ng-click="startRenaming()", - ng-show="!state.renaming" - ) - i.fa.fa-pencil - - .toolbar-right - span.online-users( - ng-show="onlineUsersArray.length > 0" - ng-controller="OnlineUsersController" - ) - span(ng-if="onlineUsersArray.length < 4") - span.online-user( - ng-repeat="user in onlineUsersArray", - ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", - popover="{{ user.name }}" - popover-placement="bottom" - popover-append-to-body="true" - popover-trigger="mouseenter" - ng-click="gotoUser(user)" - ) {{ user.name.slice(0,1) }} - - span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") - span.online-user.online-user-multi( - dropdown-toggle, - tooltip="#{translate('connected_users')}", - tooltip-placement="left" - ) - strong {{ onlineUsersArray.length }} - i.fa.fa-fw.fa-user - ul.dropdown-menu.pull-right - li.dropdown-header #{translate('connected_users')} - li(ng-repeat="user in onlineUsersArray") - a(href, ng-click="gotoUser(user)") - span.online-user( - ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" - ) {{ user.name.slice(0,1) }} - | {{ user.name }} - - - a.btn.btn-full-height( - href, - ng-if="permissions.admin", - tooltip="#{translate('share')}", - tooltip-placement="bottom", - ng-click="openShareProjectModal()", - ng-controller="ShareController" - ) - i.fa.fa-fw.fa-group - a.btn.btn-full-height( - href, - ng-click="toggleTrackChanges()", - ng-class="{ active: (ui.view == 'track-changes') }" - tooltip="#{translate('recent_changes')}", - tooltip-placement="bottom" - ) - i.fa.fa-fw.fa-history - a.btn.btn-full-height( - href, - tooltip="#{translate('chat')}", - tooltip-placement="bottom", - ng-class="{ active: ui.chatOpen }", - ng-click="toggleChat()", - ng-controller="ChatButtonController", - ng-show="!anonymous" - ) - i.fa.fa-fw.fa-comment( - ng-class="{ 'bounce': unreadMessages > 0 }" + i.fa.fa-fw.fa-level-up + + span(ng-controller="PdfViewToggleController") + a( + href, + ng-show="ui.pdfLayout == 'flat' && fileTreeClosed", + tooltip="PDF", + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="togglePdfView()", + ng-class="{ 'active': ui.view == 'pdf' }" ) - span.label.label-info( - ng-show="unreadMessages > 0" - ) {{ unreadMessages }} \ No newline at end of file + i.fa.fa-file-pdf-o + + .toolbar-center.project-name(ng-controller="ProjectNameController") + span.name( + ng-dblclick="!permissions.admin || startRenaming()", + ng-show="!state.renaming" + ) {{ project.name }} + + input.form-control( + type="text" + ng-model="inputs.name", + ng-show="state.renaming", + on-enter="finishRenaming()", + ng-blur="finishRenaming()", + select-name-when="state.renaming" + ) + + a.rename( + ng-if="permissions.admin", + href='#', + tooltip-placement="bottom", + tooltip="#{translate('rename')}", + tooltip-append-to-body="true", + ng-click="startRenaming()", + ng-show="!state.renaming" + ) + i.fa.fa-pencil + + .toolbar-right + span.online-users( + ng-show="onlineUsersArray.length > 0" + ng-controller="OnlineUsersController" + ) + span(ng-if="onlineUsersArray.length < 4") + span.online-user( + ng-repeat="user in onlineUsersArray", + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", + popover="{{ user.name }}" + popover-placement="bottom" + popover-append-to-body="true" + popover-trigger="mouseenter" + ng-click="gotoUser(user)" + ) {{ user.name.slice(0,1) }} + + span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") + span.online-user.online-user-multi( + dropdown-toggle, + tooltip="#{translate('connected_users')}", + tooltip-placement="left" + ) + strong {{ onlineUsersArray.length }} + i.fa.fa-fw.fa-user + ul.dropdown-menu.pull-right + li.dropdown-header #{translate('connected_users')} + li(ng-repeat="user in onlineUsersArray") + a(href, ng-click="gotoUser(user)") + span.online-user( + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" + ) {{ user.name.slice(0,1) }} + | {{ user.name }} + + + a.btn.btn-full-height( + href, + ng-if="permissions.admin", + tooltip="#{translate('share')}", + tooltip-placement="bottom", + ng-click="openShareProjectModal()", + ng-controller="ShareController", + ) + i.fa.fa-fw.fa-group + a.btn.btn-full-height( + href, + ng-click="toggleHistory()", + ng-class="{ active: (ui.view == 'history') }" + tooltip="#{translate('recent_changes')}", + tooltip-placement="bottom", + ) + i.fa.fa-fw.fa-history + a.btn.btn-full-height( + href, + tooltip="#{translate('chat')}", + tooltip-placement="bottom", + ng-class="{ active: ui.chatOpen }", + ng-click="toggleChat()", + ng-controller="ChatButtonController", + ng-show="!anonymous", + ) + i.fa.fa-fw.fa-comment( + ng-class="{ 'bounce': unreadMessages > 0 }" + ) + span.label.label-info( + ng-show="unreadMessages > 0" + ) {{ unreadMessages }} + +div(ng-if="shouldABTestHeaderLabels") + div(sixpack-switch="editor-header") + header.toolbar.toolbar-header( + ng-cloak, + ng-hide="state.loading" + sixpack-default + ) + a.btn.btn-full-height( + href, + ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');" + tooltip='#{translate("menu")}', + tooltip-placement="bottom", + tooltip-append-to-body="true", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-bars + a( + href="/project" + tooltip="#{translate('back_to_projects')}", + tooltip-placement="bottom", + tooltip-append-to-body="true" + ) + i.fa.fa-fw.fa-level-up + + span(ng-controller="PdfViewToggleController") + a( + href, + ng-show="ui.pdfLayout == 'flat' && fileTreeClosed", + tooltip="PDF", + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="togglePdfView()", + ng-class="{ 'active': ui.view == 'pdf' }" + ) + i.fa.fa-file-pdf-o + + .toolbar-center.project-name(ng-controller="ProjectNameController") + span.name( + ng-dblclick="!permissions.admin || startRenaming()", + ng-show="!state.renaming" + ) {{ project.name }} + + input.form-control( + type="text" + ng-model="inputs.name", + ng-show="state.renaming", + on-enter="finishRenaming()", + ng-blur="finishRenaming()", + select-name-when="state.renaming" + ) + + a.rename( + ng-if="permissions.admin", + href='#', + tooltip-placement="bottom", + tooltip="#{translate('rename')}", + tooltip-append-to-body="true", + ng-click="startRenaming()", + ng-show="!state.renaming" + ) + i.fa.fa-pencil + + .toolbar-right + span.online-users( + ng-show="onlineUsersArray.length > 0" + ng-controller="OnlineUsersController" + ) + span(ng-if="onlineUsersArray.length < 4") + span.online-user( + ng-repeat="user in onlineUsersArray", + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", + popover="{{ user.name }}" + popover-placement="bottom" + popover-append-to-body="true" + popover-trigger="mouseenter" + ng-click="gotoUser(user)" + ) {{ user.name.slice(0,1) }} + + span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") + span.online-user.online-user-multi( + dropdown-toggle, + tooltip="#{translate('connected_users')}", + tooltip-placement="left" + ) + strong {{ onlineUsersArray.length }} + i.fa.fa-fw.fa-user + ul.dropdown-menu.pull-right + li.dropdown-header #{translate('connected_users')} + li(ng-repeat="user in onlineUsersArray") + a(href, ng-click="gotoUser(user)") + span.online-user( + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" + ) {{ user.name.slice(0,1) }} + | {{ user.name }} + + + a.btn.btn-full-height( + href, + ng-if="permissions.admin", + tooltip="#{translate('share')}", + tooltip-placement="bottom", + ng-click="openShareProjectModal(); trackABTestConversion('share');", + ng-controller="ShareController", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-group + a.btn.btn-full-height( + href, + ng-click="toggleHistory(); trackABTestConversion('history');", + ng-class="{ active: (ui.view == 'history') }" + tooltip="#{translate('recent_changes')}", + tooltip-placement="bottom", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-history + a.btn.btn-full-height( + href, + tooltip="#{translate('chat')}", + tooltip-placement="bottom", + ng-class="{ active: ui.chatOpen }", + ng-click="toggleChat(); trackABTestConversion('chat');", + ng-controller="ChatButtonController", + ng-show="!anonymous", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-comment( + ng-class="{ 'bounce': unreadMessages > 0 }" + ) + span.label.label-info( + ng-show="unreadMessages > 0" + ) {{ unreadMessages }} + + header.toolbar.toolbar-header.toolbar-with-labels( + ng-cloak, + ng-hide="state.loading" + sixpack-when="labels" + ) + .toolbar-left + a.btn.btn-full-height( + href, + ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-bars + p.toolbar-label #{translate("menu")} + a( + href="/project" + ) + i.fa.fa-fw.fa-level-up + + span(ng-controller="PdfViewToggleController") + a( + href, + ng-show="ui.pdfLayout == 'flat' && fileTreeClosed", + tooltip="PDF", + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="togglePdfView()", + ng-class="{ 'active': ui.view == 'pdf' }" + ) + i.fa.fa-file-pdf-o + + .toolbar-center.project-name(ng-controller="ProjectNameController") + span.name( + ng-dblclick="!permissions.admin || startRenaming()", + ng-show="!state.renaming" + ) {{ project.name }} + + input.form-control( + type="text" + ng-model="inputs.name", + ng-show="state.renaming", + on-enter="finishRenaming()", + ng-blur="finishRenaming()", + select-name-when="state.renaming" + ) + + a.rename( + ng-if="permissions.admin", + href='#', + tooltip-placement="bottom", + tooltip="#{translate('rename')}", + tooltip-append-to-body="true", + ng-click="startRenaming()", + ng-show="!state.renaming" + ) + i.fa.fa-pencil + + .toolbar-right + span.online-users( + ng-show="onlineUsersArray.length > 0" + ng-controller="OnlineUsersController" + ) + span(ng-if="onlineUsersArray.length < 4") + span.online-user( + ng-repeat="user in onlineUsersArray", + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", + popover="{{ user.name }}" + popover-placement="bottom" + popover-append-to-body="true" + popover-trigger="mouseenter" + ng-click="gotoUser(user)" + ) {{ user.name.slice(0,1) }} + + span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4") + span.online-user.online-user-multi( + dropdown-toggle, + tooltip="#{translate('connected_users')}", + tooltip-placement="left" + ) + strong {{ onlineUsersArray.length }} + i.fa.fa-fw.fa-user + ul.dropdown-menu.pull-right + li.dropdown-header #{translate('connected_users')} + li(ng-repeat="user in onlineUsersArray") + a(href, ng-click="gotoUser(user)") + span.online-user( + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" + ) {{ user.name.slice(0,1) }} + | {{ user.name }} + + + a.btn.btn-full-height( + href, + ng-if="permissions.admin", + ng-click="openShareProjectModal(); trackABTestConversion('share');", + ng-controller="ShareController", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-group + p.toolbar-label #{translate("share")} + a.btn.btn-full-height( + href, + ng-click="toggleHistory(); trackABTestConversion('history');", + ng-class="{ active: (ui.view == 'history') }", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-history + p.toolbar-label #{translate("history")} + a.btn.btn-full-height( + href, + ng-class="{ active: ui.chatOpen }", + ng-click="toggleChat(); trackABTestConversion('chat');", + ng-controller="ChatButtonController", + ng-show="!anonymous", + sixpack-convert="editor-header" + ) + i.fa.fa-fw.fa-comment( + ng-class="{ 'bounce': unreadMessages > 0 }" + ) + span.label.label-info( + ng-show="unreadMessages > 0" + ) {{ unreadMessages }} + p.toolbar-label #{translate("chat")} \ No newline at end of file diff --git a/services/web/app/views/project/editor/track-changes.jade b/services/web/app/views/project/editor/history.jade similarity index 73% rename from services/web/app/views/project/editor/track-changes.jade rename to services/web/app/views/project/editor/history.jade index dc7ce38708..9cf756c344 100644 --- a/services/web/app/views/project/editor/track-changes.jade +++ b/services/web/app/views/project/editor/history.jade @@ -1,5 +1,5 @@ -div#trackChanges(ng-show="ui.view == 'track-changes'") - span(ng-controller="TrackChangesPremiumPopup") +div#history(ng-show="ui.view == 'history'") + span(ng-controller="HistoryPremiumPopup") .upgrade-prompt(ng-show="!project.features.versioning") .message(ng-show="project.owner._id == user.id") p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})} @@ -33,29 +33,29 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") a.btn.btn-success( href ng-class="buttonClass" - ng-click="startFreeTrial('track-changes')" + ng-click="startFreeTrial('history')" ) #{translate("start_free_trial")} .message(ng-show="project.owner._id != user.id") p #{translate("ask_proj_owner_to_upgrade_for_history")} p - a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")} + a.small(href, ng-click="toggleHistory()") #{translate("cancel")} aside.change-list( - ng-controller="TrackChangesListController" + ng-controller="HistoryListController" infinite-scroll="loadMore()" - infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd" - infinite-scroll-initialize="ui.view == 'track-changes'" + infinite-scroll-disabled="history.loading || history.atEnd" + infinite-scroll-initialize="ui.view == 'history'" ) .infinite-scroll-inner ul.list-unstyled( ng-class="{\ - 'hover-state': trackChanges.hoveringOverListSelectors\ + 'hover-state': history.hoveringOverListSelectors\ }" ) li.change( - ng-repeat="update in trackChanges.updates" + ng-repeat="update in history.updates" ng-class="{\ 'first-in-day': update.meta.first_in_day,\ 'selected': update.inSelection,\ @@ -65,7 +65,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") 'hover-selected-to': update.hoverSelectedTo,\ 'hover-selected-from': update.hoverSelectedFrom,\ }" - ng-controller="TrackChangesListItemController" + ng-controller="HistoryListItemController" ) div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }} @@ -108,57 +108,69 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") .color-square(style="background-color: hsl(100, 100%, 50%)") span #{translate("anonymous")} - .loading(ng-show="trackChanges.loading") + .loading(ng-show="history.loading") i.fa.fa-spin.fa-refresh | #{translate("loading")}... - .diff-panel.full-size(ng-controller="TrackChangesDiffController") + .diff-panel.full-size(ng-controller="HistoryDiffController") .diff( - ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error" + ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error" ) .toolbar.toolbar-alt span.name - | {{trackChanges.diff.highlights.length}} + | {{history.diff.highlights.length}} ng-pluralize( - count="trackChanges.diff.highlights.length", + count="history.diff.highlights.length", when="{\ 'one': 'change',\ 'other': 'changes'\ }" ) - | in {{trackChanges.diff.doc.name}} + | in {{history.diff.doc.name}} .toolbar-right a.btn.btn-danger.btn-sm( href, ng-click="openRestoreDiffModal()" ) #{translate("restore_to_before_these_changes")} .diff-editor.hide-ace-cursor( - ace-editor="track-changes", + ace-editor="history", theme="settings.theme", font-size="settings.fontSize", - text="trackChanges.diff.text", - highlights="trackChanges.diff.highlights", + text="history.diff.text", + highlights="history.diff.highlights", read-only="true", resize-on="layout:main:resize", navigate-highlights="true" ) .diff-deleted.text-centered( - ng-show="trackChanges.diff.deleted" + ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess" ) - p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})} - + p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})} p a.btn.btn-primary.btn-lg( href, - ng-click="restoreDeletedDoc()" + ng-click="restoreDeletedDoc()", + ng-disabled="history.diff.restoreInProgress" ) #{translate("restore")} - .loading-panel(ng-show="trackChanges.diff.loading") + + .diff-deleted.text-centered( + ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess" + ) + p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})} + p.text-serif #{translate("file_restored_back_to_editor")} + p + a.btn.btn-default( + href, + ng-click="backToEditorAfterRestore()", + ) #{translate("file_restored_back_to_editor_btn")} + + .loading-panel(ng-show="history.diff.loading") i.fa.fa-spin.fa-refresh | #{translate("loading")}... - .error-panel(ng-show="trackChanges.diff.error") + .error-panel(ng-show="history.diff.error") .alert.alert-danger #{translate("generic_something_went_wrong")} -script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate") +script(type="text/ng-template", id="historyRestoreDiffModalTemplate") .modal-header button.close( type="button" diff --git a/services/web/app/views/project/editor/hotkeys.jade b/services/web/app/views/project/editor/hotkeys.jade index c2c5e66759..15153cdccf 100644 --- a/services/web/app/views/project/editor/hotkeys.jade +++ b/services/web/app/views/project/editor/hotkeys.jade @@ -66,6 +66,31 @@ script(type="text/ng-template", id="hotkeysModalTemplate") .hotkey span.combination {{ctrl}} + I span.description Italic Text + + h3 #{translate("autocomplete")} + .row + .col-xs-6 + .hotkey + span.combination Ctrl + Space + span.description Autocomplete Menu + + .col-xs-6 + .hotkey + span.combination Tab / Up / Down + span.description Select Candidate + + .hotkey + span.combination Enter + span.description Insert Candidate + + h3 !{translate("autocomplete_references")} + .row + .col-xs-6 + .hotkey + span.combination Ctrl + Space + span.description Search References + + .modal-footer button.btn.btn-default( ng-click="cancel()" diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index 05fa19542f..20b0905abf 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -105,6 +105,15 @@ aside#left-menu.full-size( ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" ) + if (user.betaProgram) + .form-controls + label(for="syntaxValidation") #{translate("syntax_validation")} + select( + name="syntaxValidation" + ng-model="settings.syntaxValidation" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + .form-controls label(for="theme") #{translate("theme")} select( @@ -197,6 +206,10 @@ script(type='text/ng-template', id='wordCountModalTemplate') ) div(ng-if="!status.loading") .container-fluid + .row(ng-show='data.messages.length > 0') + .col-xs-12 + .alert.alert-danger + p(style="white-space: pre-wrap") {{data.messages}} .row .col-xs-4 .pull-right #{translate("total_words")} : diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index 200c609d97..074856bd7c 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController") dropdown-toggle ) span.caret - ul.dropdown-menu.dropdown-menu-right + ul.dropdown-menu.dropdown-menu-left li.dropdown-header #{translate("compile_mode")} li a(href, ng-click="draft = false") @@ -36,6 +36,19 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-fw(ng-class="{'fa-check': draft}") | #{translate("fast")} span.subdued [draft] + li.dropdown-header #{translate("compile_time_checks")} + li + a(href, ng-click="stop_on_validation_error = true") + i.fa.fa-fw(ng-class="{'fa-check': stop_on_validation_error}") + | #{translate("stop_on_validation_error")} + li + a(href, ng-click="stop_on_validation_error = false") + i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}") + | #{translate("ignore_validation_errors")} + li + a(href, ng-click="recompile({check:true})") + i.fa.fa-fw() + | #{translate("run_syntax_check_now")} a( href ng-click="stop()" @@ -100,6 +113,23 @@ div.full-size.pdf(ng-controller="PdfController") strong #{translate("compile_error")}. span #{translate("generic_failed_compile_message")}. + .alert.alert-danger(ng-show="pdf.failedCheck") + strong #{translate("failed_compile_check")}. + p + p.text-center(ng-show="!check") + a.text-info( + href, + ng-disabled="pdf.compiling", + ng-click="recompile({try:true})" + ) #{translate("failed_compile_check_try")} + | #{translate("failed_compile_option_or")} + a.text-info( + href, + ng-disabled="pdf.compiling", + ng-click="recompile({force:true})" + ) #{translate("failed_compile_check_ignore")} + | . + div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController") .alert( ng-class="{\ @@ -115,7 +145,8 @@ div.full-size.pdf(ng-controller="PdfController") | span(ng-show="entry.file") {{ entry.file }} span(ng-show="entry.line") , line {{ entry.line }} - p.entry-message(ng-show="entry.message") {{ entry.message }} + p.entry-message(ng-show="entry.message") + | {{ entry.type }} {{ entry.message }} .card.card-hint( ng-if="entry.humanReadableHint" stop-propagation="click" @@ -126,7 +157,7 @@ div.full-size.pdf(ng-controller="PdfController") ng-show="entry.humanReadableHint", ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)") .card-hint-actions.clearfix - .card-hint-ext-link(ng-if="wikiEnabled") + .card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL") a( ng-href="{{ entry.extraInfoURL }}", ng-click="trackLogHintsLearnMore()" @@ -198,7 +229,7 @@ div.full-size.pdf(ng-controller="PdfController") p.entry-content(ng-show="entry.content") {{ entry.content.trim() }} - p + div .files-dropdown-container a.btn.btn-default.btn-sm( href, @@ -313,7 +344,7 @@ div.full-size.pdf(ng-controller="PdfController") strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")} p #{translate("free_accounts_have_timeout_upgrade_to_increase")} p Plus: - p + div ul.list-unstyled li i.fa.fa-check diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index ad3b2bd9ba..fd13ccb240 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ) #{translate("make_private")} .row.project-member .col-xs-8 {{ project.owner.email }} - .text-right( + .text-left( ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}" ) #{translate("owner")} .row.project-member(ng-repeat="member in project.members") .col-xs-8 {{ member.email }} - .col-xs-3.text-right + .col-xs-3.text-left span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")} span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")} .col-xs-1 @@ -43,6 +43,23 @@ script(type='text/ng-template', id='shareProjectModalTemplate') ng-click="removeMember(member)" ) i.fa.fa-times + .row.project-invite(ng-repeat="invite in project.invites") + .col-xs-8 {{ invite.email }} + div.small + | #{translate("invite_not_accepted")}. + a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")} + .col-xs-3.text-left + // todo: get invite privileges + span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")} + span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")} + .col-xs-1 + a( + href + tooltip="#{translate('revoke_invite')}" + tooltip-placement="bottom" + ng-click="revokeInvite(invite)" + ) + i.fa.fa-times .row.invite-controls form(ng-show="canAddCollaborators") .small #{translate("share_with_your_collabs")} @@ -78,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') button.btn.btn-info( type="submit" ng-mousedown="addMembers()" + ng-keyup="$event.keyCode == 13 ? addMembers() : null" ) #{translate("share")} div(ng-hide="canAddCollaborators") p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also: @@ -123,7 +141,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .modal-footer-left i.fa.fa-refresh.fa-spin(ng-show="state.inflight") span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} - button.btn.btn-primary( + button.btn.btn-default( ng-click="done()" ) #{translate("close")} diff --git a/services/web/app/views/project/invite/not-valid.jade b/services/web/app/views/project/invite/not-valid.jade new file mode 100644 index 0000000000..4d6b7f869e --- /dev/null +++ b/services/web/app/views/project/invite/not-valid.jade @@ -0,0 +1,18 @@ +extends ../../layout + +block content + .content.content-alt + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-invalid + .page-header.text-centered + h1 #{translate("invite_not_valid")} + .row.text-center + .col-md-12 + p + | #{translate("invite_not_valid_description")}. + .row.text-center.actions + .col-md-12 + a.btn.btn-info(href="/project") #{translate("back_to_your_projects")} + \ No newline at end of file diff --git a/services/web/app/views/project/invite/show.jade b/services/web/app/views/project/invite/show.jade new file mode 100644 index 0000000000..eed30d3d19 --- /dev/null +++ b/services/web/app/views/project/invite/show.jade @@ -0,0 +1,31 @@ +extends ../../layout + +block content + .content.content-alt + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-accept + .page-header.text-centered + h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})} + em + span.project-name #{project.name} + .row.text-center + .col-md-12 + p + | #{translate("accepting_invite_as")} + em #{user.email} + .row + .col-md-12 + form.form( + name="acceptForm", + method="POST", + action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept" + ) + input(name='_csrf', type='hidden', value=csrfToken) + input(name='token', type='hidden', value="#{invite.token}") + .form-group.text-center + button.btn.btn-lg.btn-primary(type="submit") + | #{translate("join_project")} + .form-group.text-center + \ No newline at end of file diff --git a/services/web/app/views/project/list.jade b/services/web/app/views/project/list.jade index 1c80b9a7a2..a6a3957720 100644 --- a/services/web/app/views/project/list.jade +++ b/services/web/app/views/project/list.jade @@ -23,7 +23,7 @@ block content .container .row(ng-cloak) - span(ng-show="first_sign_up == 'default' || projects.length > 0") + span(ng-if="projects.length > 0") aside.col-md-2.col-xs-3 include ./list/side-bar @@ -31,8 +31,8 @@ block content include ./list/notifications include ./list/project-list - span(ng-if="first_sign_up == 'minimial' && projects.length == 0") + span(ng-if="projects.length === 0") .col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8 - include ./list/project-list-minimal + include ./list/empty-project-list include ./list/modals \ No newline at end of file diff --git a/services/web/app/views/project/list/empty-project-list.jade b/services/web/app/views/project/list/empty-project-list.jade new file mode 100644 index 0000000000..a6c6dcd299 --- /dev/null +++ b/services/web/app/views/project/list/empty-project-list.jade @@ -0,0 +1,46 @@ +.row.row-spaced + .col-xs-12 + .card.card-thin.project-list-card + div.welcome.text-centered(ng-cloak) + h2 #{translate("welcome_to_sl")} + p #{translate("new_to_latex_look_at")} + a(href="/templates") #{translate("templates").toLowerCase()} + | #{translate("or")} + a(href="/learn") #{translate("latex_help_guide")} + + + .row + .col-md-offset-4.col-md-4 + .dropdown.minimal-create-proj-dropdown(dropdown) + a.btn.btn-success.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | Create First Project + + ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu") + li + a( + href, + ng-click="openCreateProjectModal()" + ) #{translate("blank_project")} + li + a( + href, + ng-click="openCreateProjectModal('example')" + ) #{translate("example_project")} + li + a( + href, + ng-click="openUploadProjectModal()" + ) #{translate("upload_project")} + != moduleIncludes("newProjectMenu", locals) + if (templates) + li.divider + li.dropdown-header #{translate("templates")} + each item in templates + li + a.menu-indent(href=item.url) #{translate(item.name)} + + diff --git a/services/web/app/views/project/list/notifications.jade b/services/web/app/views/project/list/notifications.jade index 29f371b724..f4be585f7b 100644 --- a/services/web/app/views/project/list/notifications.jade +++ b/services/web/app/views/project/list/notifications.jade @@ -4,12 +4,31 @@ span(ng-controller="NotificationsController").userNotifications ng-cloak ) li.notification_entry( - ng-repeat="unreadNotification in notifications", + ng-repeat="notification in notifications", ) - .row(ng-hide="unreadNotification.hide") + .row(ng-hide="notification.hide") .col-xs-12 - .alert.alert-info - span(ng-bind-html="unreadNotification.html") - button(ng-click="dismiss(unreadNotification)").close.pull-right - span(aria-hidden="true") × - span.sr-only #{translate("close")} + .alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController") + div.notification_inner + .notification_body(ng-show="!notification.accepted") + | !{translate("notification_project_invite_message")} + a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight") + span(ng-show="!notification.inflight") #{translate("join_project")} + span(ng-show="notification.inflight") + i.fa.fa-fw.fa-spinner.fa-spin + | + | #{translate("joining")}... + .notification_body(ng-show="notification.accepted") + | !{translate("notification_project_invite_accepted_message")} + a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")} + span().notification_close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info(ng-if="notification.templateKey != 'notification_project_invite'") + div.notification_inner + span(ng-bind-html="notification.html").notification_body + span().notification_close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} diff --git a/services/web/app/views/project/list/project-list-minimal.jade b/services/web/app/views/project/list/project-list-minimal.jade deleted file mode 100644 index b73210f016..0000000000 --- a/services/web/app/views/project/list/project-list-minimal.jade +++ /dev/null @@ -1,202 +0,0 @@ -.row - .col-xs-12(ng-cloak) - - .project-tools(ng-cloak) - .btn-toolbar(ng-show="filter != 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href='#', - tooltip="#{translate('download')}", - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="downloadSelectedProjects()" - ) - i.fa.fa-cloud-download - a.btn.btn-default( - href='#', - tooltip="#{translate('delete')}", - tooltip-placement="bottom", - tooltip-append-to-body="true", - ng-click="openArchiveProjectsModal()" - ) - i.fa.fa-trash-o - - .btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown) - a.btn.btn-default.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle, - tooltip="#{translate('add_to_folders')}", - tooltip-append-to-body="true", - tooltip-placement="bottom" - ) - i.fa.fa-folder-open-o - | - span.caret - ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu( - role="menu" - ng-controller="TagListController" - ) - li.dropdown-header #{translate("add_to_folder")} - li( - ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'", - ng-controller="TagDropdownItemController" - ) - a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") - i.fa( - ng-class="{\ - 'fa-check-square-o': areSelectedProjectsInTag == true,\ - 'fa-square-o': areSelectedProjectsInTag == false,\ - 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ - }" - ) - | {{tag.name}} - li.divider - li - a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} - - .btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown - a.btn.btn-default.dropdown-toggle( - href='#', - data-toggle="dropdown", - dropdown-toggle - ) #{translate("more")} - span.caret - ul.dropdown-menu.dropdown-menu-right(role="menu") - li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") - a( - href='#', - ng-click="openRenameProjectModal()" - ) #{translate("rename")} - li - a( - href='#', - ng-click="openCloneProjectModal()" - ) #{translate("make_copy")} - - .btn-toolbar(ng-show="filter == 'archived'") - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-default( - href='#', - data-original-title="Restore", - data-toggle="tooltip", - data-placement="bottom", - ng-click="restoreSelectedProjects()" - ) #{translate("restore")} - - .btn-group(ng-hide="selectedProjects.length < 1") - a.btn.btn-danger( - href='#', - data-original-title="Delete Forever", - data-toggle="tooltip", - data-placement="bottom", - ng-click="openDeleteProjectsModal()" - ) #{translate("delete_forever")} - -.row.row-spaced - .col-xs-12 - .card.card-thin.project-list-card - ul.list-unstyled.project-list.structured-list( - select-all-list, - ng-if="projects.length > 0", - max-height="projectListHeight - 25", - ng-cloak - ) - li.container-fluid - .row - .col-xs-6 - input.select-all( - select-all, - type="checkbox" - ) - span.header.clickable(ng-click="changePredicate('name')") #{translate("title")} - i.tablesort.fa(ng-class="getSortIconClass('name')") - .col-xs-2 - span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")} - i.tablesort.fa(ng-class="getSortIconClass('accessLevel')") - .col-xs-4 - span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} - i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')") - li.project_entry.container-fluid( - ng-repeat="project in visibleProjects | orderBy:predicate:reverse", - ng-controller="ProjectListItemController" - ) - .row - .col-xs-6 - input.select-item( - select-individual, - type="checkbox", - ng-model="project.selected" - ) - span - a.projectName(href="/project/{{project.id}}") {{project.name}} - span( - ng-controller="TagListController" - ) - a.label.label-default.tag-label( - href, - ng-repeat='tag in project.tags', - ng-click="selectTag(tag)" - ) {{tag.name}} - .col-xs-2 - span.owner {{ownerName()}} - .col-xs-4 - span.last-modified {{project.lastUpdated | formatDate}} - li( - ng-if="visibleProjects.length == 0", - ng-cloak - ) - .row - .col-xs-12.text-centered - small #{translate("no_projects")} - - div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) - h2 #{translate("welcome_to_sl")} - p #{translate("new_to_latex_look_at")} - a(href="/templates") #{translate("templates").toLowerCase()} - | #{translate("or")} - a(href="/learn") #{translate("latex_help_guide")} - - - .row - .col-md-offset-4.col-md-4 - .dropdown(dropdown) - a.btn.btn-success.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle - ) - | Create First Project - style. - .dropdown{text-align:center;} - .button, .dropdown-menu{margin:2px auto} - .dropdown-menu{width:200px; left:50%; margin-left:-100px;} - - ul.dropdown-menu(role="menu", style="text-align:center;") - li - a( - href, - ng-click="openCreateProjectModal()" - sixpack-convert="first_sign_up", - ) #{translate("blank_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openCreateProjectModal('example')" - ) #{translate("example_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openUploadProjectModal()" - ) #{translate("upload_project")} - != moduleIncludes("newProjectMenu", locals) - if (templates) - li.divider - li.dropdown-header #{translate("templates")} - each item in templates - li - a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} - - diff --git a/services/web/app/views/project/list/project-list.jade b/services/web/app/views/project/list/project-list.jade index 649c860b26..01007213d0 100644 --- a/services/web/app/views/project/list/project-list.jade +++ b/services/web/app/views/project/list/project-list.jade @@ -8,7 +8,7 @@ input.form-control.col-md-7.col-xs-12( placeholder="#{translate('search_projects')}…", autofocus='autofocus', - ng-model="searchText", + ng-model="searchText.value", focus-on='search:clear', ng-keyup="searchProjects()" ) @@ -16,7 +16,7 @@ i.fa.fa-times.form-control-feedback( ng-click="clearSearchText()", style="cursor: pointer;", - ng-show="searchText.length > 0" + ng-show="searchText.value.length > 0" ) //- i.fa.fa-remove diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade index 96365c813e..3738e9a4ae 100644 --- a/services/web/app/views/project/list/side-bar.jade +++ b/services/web/app/views/project/list/side-bar.jade @@ -155,6 +155,6 @@ | #{translate("or_unlock_features_bonus")} a(href="/user/bonus") #{translate("sharing_sl")} . script. - window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription} + window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)} diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade index 9b9464419e..34493e5a8c 100644 --- a/services/web/app/views/subscriptions/dashboard.jade +++ b/services/web/app/views/subscriptions/dashboard.jade @@ -40,6 +40,11 @@ block content .container(ng-controller="UserSubscriptionController") .row .col-md-8.col-md-offset-2 + if saved_billing_details + .alert.alert-success + i.fa.fa-check + | + | #{translate("your_billing_details_were_saved")} .card(ng-if="view == 'overview'") .page-header h1 #{translate("your_subscription")} diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.jade index f0b6671bcd..cc41e0a4b9 100644 --- a/services/web/app/views/subscriptions/edit-billing-details.jade +++ b/services/web/app/views/subscriptions/edit-billing-details.jade @@ -1,10 +1,9 @@ extends ../layout -block content - - locals.supressDefaultJs = true - script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) - script(src=buildJsPath('libs/recurly.min.js')) +block scripts + script(src=buildJsPath('libs/recurly.min.js', {fingerprint:false})) +block content .content.content-alt .container .row @@ -19,7 +18,7 @@ block content Recurly.config(!{recurlyConfig}) Recurly.buildBillingInfoUpdateForm({ target : "#billingDetailsForm", - successURL : "#{successURL}?_csrf=#{csrfToken}", + successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails", signature : "!{signature}", accountCode : "#{user.id}" }); diff --git a/services/web/app/views/subscriptions/group/invite.jade b/services/web/app/views/subscriptions/group/invite.jade index 3ea26aa0a1..3c67a79f81 100644 --- a/services/web/app/views/subscriptions/group/invite.jade +++ b/services/web/app/views/subscriptions/group/invite.jade @@ -3,7 +3,7 @@ extends ../../layout block scripts script(type='text/javascript'). window.group_subscription_id = '#{group_subscription_id}' - window.has_personal_subscription = '#{has_personal_subscription}' + window.has_personal_subscription = #{has_personal_subscription} block content .content.content-alt diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade index e84dc56eec..1465b24c82 100644 --- a/services/web/app/views/subscriptions/new.jade +++ b/services/web/app/views/subscriptions/new.jade @@ -1,6 +1,6 @@ extends ../layout -block scripts +block scripts script(src="https://js.recurly.com/v3/recurly.js") script(type='text/javascript'). @@ -11,10 +11,6 @@ block scripts window.couponCode = "#{couponCode}" block content - - locals.supressDefaultJs = true - script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) - - .content.content-alt .container(ng-controller="NewSubscriptionController" ng-cloak) .row.card-group @@ -51,226 +47,218 @@ block content div(ng-if="normalPrice") span.small Normally {{price.currency.symbol}}{{normalPrice}} .row - .col-md-12 - form(ng-show="planName") - + div() + .col-md-12() + form( + ng-if="planName" + name="simpleCCForm" + novalidate + ) + div.payment-method-toggle + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('credit_card');" + ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-mastercard.fa-2x + span + i.fa.fa-cc-visa.fa-2x + span + i.fa.fa-cc-amex.fa-2x + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('paypal');" + ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-paypal.fa-2x + + .alert.alert-warning.small(ng-show="genericError") + strong {{genericError}} - .row - .col-md-12 - .form-group - .row - .col-md-6 - label.radio-inline - input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod") - i.fa.fa-cc-mastercard.fa-3x - span - i.fa.fa-cc-visa.fa-3x - .col-md-6 - label.radio-inline - input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod") - i.fa.fa-cc-paypal.fa-3x + div(ng-if="paymentMethod.value === 'credit_card'") + .row + .col-xs-6 + .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") + label(for="first-name") #{translate('first_name')} + input#first-name.form-control( + type="text" + maxlength='255' + data-recurly="first_name" + name="firstName" + ng-model="data.first_name" + required + ) + span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }} + .col-xs-6 + .form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''") + label(for="last-name") #{translate('last_name')} + input#last-name.form-control( + type="text" + maxlength='255' + data-recurly="last_name" + name="lastName" + ng-model="data.last_name" + required + ) + span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }} + .form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''") + label(for="card-no") #{translate("credit_card_number")} + input#card-no.form-control( + type="text" + ng-model="data.number" + name="ccNumber" + ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;" + ng-blur="validateCardNumber();" + required + cc-format-card-number + ) + span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }} + .row + .col-xs-6 + .form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''") + label #{translate("expiry")} + input.form-control( + type="text" + ng-model="data.mmYY" + name="expiry" + placeholder="MM / YY" + ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;" + ng-blur="updateExpiry(); validateExpiry()" + required + cc-format-expiry + ) + span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }} - .alert.alert-warning.small(ng-show="genericError") - strong {{genericError}} + .col-xs-6 + .form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''") + label #{translate("security_code")} + input.form-control( + type="text" + ng-model="data.cvv" + ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;" + ng-blur="validateCvv()" + name="cvv" + required + cc-format-sec-code + ) + .form-control-feedback + a.form-helper( + href + tabindex="-1" + tooltip-template="'cvv-tooltip-tpl.html'" + tooltip-trigger="mouseenter" + tooltip-append-to-body="true" + ) ? + span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }} - span(ng-hide="paymentMethod == 'paypal'") - .row - .col-md-12 - .form-group - div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV - div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")} - .row - .col-md-6 - .form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''") - input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}") - .col-md-3 - .form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''") - input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV") - .row - .col-md-12 - div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")} - .row - .col-md-3 - .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''") - select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month') - option(value="", disabled, selected) Month - option(value="01") 01 - option(value="02") 02 - option(value="03") 03 - option(value="04") 04 - option(value="05") 05 - option(value="06") 06 - option(value="07") 07 - option(value="08") 08 - option(value="09") 09 - option(value="10") 10 - option(value="11") 11 - option(value="12") 12 - .col-md-4 - .form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''") - select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year') - option(value="", disabled, selected) Year - option(value="2016") 2016 - option(value="2017") 2017 - option(value="2018") 2018 - option(value="2019") 2019 - option(value="2020") 2020 - option(value="2021") 2021 - option(value="2022") 2022 - option(value="2023") 2023 - option(value="2024") 2024 - option(value="2025") 2025 - option(value="2026") 2026 - .row - .col-md-6 - .form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}") - .col-md-6 - .form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}") - hr - .row - .col-md-12 - .form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''") - label #{translate("billing_address")} - input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}") - .form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}") - .form-group(ng-class="validation.errorFields.state ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}") - .row - .col-md-7 - .form-group(ng-class="validation.errorFields.city ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}") - .col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''") - input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}") - .row - .col-md-7 - .form-group(ng-class="validation.errorFields.country ? 'has-error' : ''") - select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required) + + div + .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") + label(for="country") #{translate('country')} + select#country.form-control( + data-recurly="country" + ng-model="data.country" + name="country" + ng-change="updateCountry()" + required + ) mixin countries_options() - .row - .col-md-8 - if (showCouponField) - .form-group - input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}") - .row - .col-md-8 + span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }} + if (showVatField) .form-group - input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}") - .row - .col-xs-7 - .form-group - button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")} - - .col-xs-3.pricingBreakdown - div(ng-if="price.next.subtotal != price.next.total") Subtotal - div(ng-if="price.next.tax!='0.00'") Tax - div - strong Total - .col-xs-2 - div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}} - div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}} - div.pull-right - strong {{price.currency.symbol}}{{price.next.total}} + label(for="vat-no") #{translate('vat_number')} + input#vat-no.form-control( + type="text" + ng-blur="applyVatNumber()" + ng-model="data.vat_number" + ) + if (showCouponField) + .form-group + label(for="coupon-code") #{translate('coupon_code')} + input#coupon-code.form-control( + type="text" + ng-blur="applyCoupon()" + ng-model="data.coupon" + ) + + p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")} + + div.price-breakdown(ng-if="price.next.tax !== '0.00'") + hr.thin + span Total: + strong {{price.currency.symbol}}{{price.next.total}} + span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax) + span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")} + span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")} + hr.thin - span(sixpack-switch="payment-left-menu-bottom") + div.payment-submit + button.btn.btn-success.btn-block( + ng-click="submit()" + ng-disabled="processing || !isFormValid(simpleCCForm);" + ) + span(ng-show="processing") + i.fa.fa-spinner.fa-spin + | + | {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }} - .col-md-3.col-md-pull-4(sixpack-default) - if showStudentPlan == 'true' - a.btn-primary.btn.plansPageStudentLink( - href, - ng-click="switchToStudent()" - ) #{translate("half_price_student")} - .card.card-first - .paymentPageFeatures - h3 #{translate("unlimited_projects")} - p #{translate("create_unlimited_projects")} + .col-md-3.col-md-pull-4 + if showStudentPlan == 'true' + a.btn-primary.btn.plansPageStudentLink( + href, + ng-click="switchToStudent()" + ) #{translate("half_price_student")} + + .card.card-first + .paymentPageFeatures + h3 #{translate("unlimited_projects")} + p #{translate("create_unlimited_projects")} + + h3 + if plan.features.collaborators == -1 + - var collaboratorCount = 'Unlimited' + else + - var collaboratorCount = plan.features.collaborators + | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} + p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time. - h3 - if plan.features.collaborators == -1 - - var collaboratorCount = 'Unlimited' - else - - var collaboratorCount = plan.features.collaborators - | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} - p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time. - - h3 #{translate("full_doc_history")} - p #{translate("see_what_has_been")} - span.added #{translate("added")} - | #{translate("and")} - span.removed #{translate("removed")}. - | #{translate("restore_to_any_older_version")}. - - h3 #{translate("sync_to_dropbox")} - p - | #{translate("acces_work_from_anywhere")}. - | #{translate("work_offline_and_sync_with_dropbox")}. - - hr - - p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days. - hr - span - a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;") - img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0") - div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;") - a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;") - .col-md-3.col-md-pull-4(sixpack-when="bolder") - if showStudentPlan == 'true' - a.btn-primary.btn.plansPageStudentLink( - href, - ng-click="switchToStudent()" - ) #{translate("half_price_student")} - - .card.card-first - .paymentPageFeatures - .page-header - h2 #{translate("features")} - h3 - i.fa.fa-check - | #{translate("unlimited_projects")} + h3 #{translate("full_doc_history")} + p #{translate("see_what_has_been")} + span.added #{translate("added")} + | #{translate("and")} + span.removed #{translate("removed")}. + | #{translate("restore_to_any_older_version")}. - h3 - i.fa.fa-check - if plan.features.collaborators == -1 - - var collaboratorCount = 'Unlimited' - else - - var collaboratorCount = plan.features.collaborators - | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} + h3 #{translate("sync_to_dropbox")} + p + | #{translate("acces_work_from_anywhere")}. + | #{translate("work_offline_and_sync_with_dropbox")}. - h3 - i.fa.fa-check - | #{translate("full_doc_history")} + hr - h3 - i.fa.fa-check - | #{translate("sync_to_dropbox")} - - h3 - i.fa.fa-check - | #{translate("sync_to_github")} - h3 - i.fa.fa-check - | #{translate("Compile Larger Projects")} - hr - - h2.text-center 30 Day Guarantee - hr - span - a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;") - img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0") - div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;") - a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;") + p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days. + hr + span + a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;") + img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0") + div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;") + a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;") script(type="text/javascript"). ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}") + script( + type="text/ng-template" + id="cvv-tooltip-tpl.html" + ) + p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card. + p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card. + mixin countries_options() option(value='', disabled, selected) #{translate("country")} option(value='-') -------------- diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade index d451c94540..9db21fa3a6 100644 --- a/services/web/app/views/subscriptions/plans.jade +++ b/services/web/app/views/subscriptions/plans.jade @@ -72,7 +72,10 @@ block content li li br - a.btn.btn-info(href="/register") #{translate("sign_up_now")} + a.btn.btn-info( + href="/register" + style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + ) #{translate("sign_up_now")} .col-md-4 .card.card-highlighted .card-header @@ -136,7 +139,10 @@ block content li li br - a.btn.btn-info(href="/register") #{translate("sign_up_now")} + a.btn.btn-info( + href="/register" + style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden") + ) #{translate("sign_up_now")} .col-md-4 .card.card-highlighted diff --git a/services/web/app/views/user/register.jade b/services/web/app/views/user/register.jade index 96db403f1c..5a1196b6a6 100644 --- a/services/web/app/views/user/register.jade +++ b/services/web/app/views/user/register.jade @@ -6,9 +6,13 @@ block content .row .registration_message if sharedProjectData.user_first_name !== undefined - h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})} - div #{translate("join_sl_to_view_project")}. - a(href="/login") #{translate("login_here")} + h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})} + em #{sharedProjectData.project_name} + div + | #{translate("join_sl_to_view_project")}. + div + | #{translate("if_you_are_registered")}, + a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} diff --git a/services/web/app/views/user/sessions.jade b/services/web/app/views/user/sessions.jade new file mode 100644 index 0000000000..948ed2d94f --- /dev/null +++ b/services/web/app/views/user/sessions.jade @@ -0,0 +1,49 @@ +extends ../layout + + +block scripts + script(type='text/javascript'). + window.otherSessions = !{JSON.stringify(sessions)} + + +block content + .content.content-alt + .container + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card.clear-user-sessions(ng-controller="ClearSessionsController", ng-cloak) + .page-header + h1 #{translate("your_sessions")} + + div + p.small + | #{translate("clear_sessions_description")} + + div + div(ng-if="state.otherSessions.length == 0") + p.text-center + | #{translate("no_other_sessions")} + + div(ng-if="state.success == true") + p.text-success.text-center + | #{translate('clear_sessions_success')} + + div(ng-if="state.otherSessions.length != 0") + table.table.table-striped + thead + tr + th #{translate("ip_address")} + th #{translate("session_created_at")} + tr(ng-repeat="session in state.otherSessions") + td {{session.ip_address}} + td {{session.session_created | formatDate}} + + p.actions + .text-center + button.btn.btn-lg.btn-primary( + ng-click="clearSessions()" + ) #{translate('clear_sessions')} + + div(ng-if="state.error == true") + p.text-danger.error + | #{translate('generic_something_went_wrong')} diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade index a23c3660aa..d2fa8326d1 100644 --- a/services/web/app/views/user/settings.jade +++ b/services/web/app/views/user/settings.jade @@ -79,7 +79,7 @@ block content required, complex-password ) - span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.currentPassword.$dirty", ng-bind-html="complexPasswordErrorMessage") + span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage") .form-group label(for='newPassword2') #{translate("confirm_new_password")} input.form-control( @@ -88,9 +88,11 @@ block content placeholder='*********', ng-model="newPassword2", equals="passwordField" - ) - span.small.text-primary(ng-show="changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty") - | #{translate("doesnt_match")} + ) + span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty") + | #{translate("doesnt_match")} + span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty") + | #{translate("Invalid Password")} .actions button.btn.btn-primary( type='submit', @@ -99,16 +101,25 @@ block content | !{moduleIncludes("userSettings", locals)} + hr + + h3 + | #{translate("sharelatex_beta_program")} + if (user.betaProgram) - hr - - h3 - | #{translate("sharelatex_beta_program")} - p.small | #{translate("beta_program_already_participating")} - div - a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")} + + div + a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")} + + hr + + h3 + | #{translate("sessions")} + + div + a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")} hr @@ -140,7 +151,7 @@ block content .modal-header h3 #{translate("delete_account")} .modal-body - p !{translate("delete_account_warning_message")} + p !{translate("delete_account_warning_message_2")} form(novalidate, name="deleteAccountForm") input.form-control( type="text", diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index f345d14129..daf16ae1f4 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -106,8 +106,8 @@ module.exports = settings = url: "" # references: # url: "http://localhost:3040" - # notifications: - # url: "http://localhost:3042" + notifications: + url: "http://localhost:3042" templates: user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2" @@ -116,7 +116,7 @@ module.exports = settings = # cdn: # web: - # host:"http://cdn.sharelatex.dev:3000" + # host:"http://nowhere.sharelatex.dev" # darkHost:"http://cdn.sharelatex.dev:3000" # Where your instance of ShareLaTeX can be found publically. Used in emails @@ -155,7 +155,7 @@ module.exports = settings = collaborators: -1 dropbox: true versioning: true - compileTimeout: 60 + compileTimeout: 180 compileGroup: "standard" references: true templates: true @@ -166,6 +166,8 @@ module.exports = settings = price: 0 features: defaultFeatures }] + + enableSubscriptions:false # i18n # ------ @@ -193,8 +195,8 @@ module.exports = settings = # passwordStrengthOptions: # pattern: "aA$3" # length: - # min: 8 - # max: 50 + # min: 1 + # max: 10 # Email support # ------------- diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json index 89d5dfe8bb..9b0cd18f50 100644 --- a/services/web/npm-shrinkwrap.json +++ b/services/web/npm-shrinkwrap.json @@ -4,64 +4,64 @@ "dependencies": { "archiver": { "version": "0.9.0", - "from": "archiver@0.9.0", + "from": "https://registry.npmjs.org/archiver/-/archiver-0.9.0.tgz", "resolved": "https://registry.npmjs.org/archiver/-/archiver-0.9.0.tgz", "dependencies": { "buffer-crc32": { "version": "0.2.5", - "from": "buffer-crc32@~0.2.1", + "from": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.5.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.5.tgz" }, "readable-stream": { "version": "1.0.34", - "from": "readable-stream@~1.0.24", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "tar-stream": { "version": "0.3.3", - "from": "tar-stream@~0.3.0", + "from": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz", "dependencies": { "bl": { "version": "0.6.0", - "from": "bl@~0.6.0", + "from": "https://registry.npmjs.org/bl/-/bl-0.6.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-0.6.0.tgz" }, "end-of-stream": { "version": "0.1.5", - "from": "end-of-stream@~0.1.3", + "from": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", "dependencies": { "once": { "version": "1.3.3", - "from": "once@~1.3.0", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } @@ -72,76 +72,76 @@ }, "zip-stream": { "version": "0.3.7", - "from": "zip-stream@~0.3.0", + "from": "https://registry.npmjs.org/zip-stream/-/zip-stream-0.3.7.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-0.3.7.tgz", "dependencies": { "crc32-stream": { "version": "0.2.0", - "from": "crc32-stream@~0.2.0", + "from": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-0.2.0.tgz", "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-0.2.0.tgz" }, "debug": { "version": "1.0.4", - "from": "debug@~1.0.2", + "from": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", "dependencies": { "ms": { "version": "0.6.2", - "from": "ms@0.6.2", + "from": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz" } } }, "deflate-crc32-stream": { "version": "0.1.2", - "from": "deflate-crc32-stream@~0.1.0", + "from": "https://registry.npmjs.org/deflate-crc32-stream/-/deflate-crc32-stream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/deflate-crc32-stream/-/deflate-crc32-stream-0.1.2.tgz" } } }, "lazystream": { "version": "0.1.0", - "from": "lazystream@~0.1.0", + "from": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz" }, "file-utils": { "version": "0.1.5", - "from": "file-utils@~0.1.5", + "from": "https://registry.npmjs.org/file-utils/-/file-utils-0.1.5.tgz", "resolved": "https://registry.npmjs.org/file-utils/-/file-utils-0.1.5.tgz", "dependencies": { "lodash": { "version": "2.1.0", - "from": "lodash@~2.1.0", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz" }, "iconv-lite": { "version": "0.2.11", - "from": "iconv-lite@~0.2.11", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" }, "glob": { "version": "3.2.11", - "from": "glob@~3.2.6", + "from": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { "version": "0.3.0", - "from": "minimatch@0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } @@ -150,95 +150,95 @@ }, "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.12", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } }, "findup-sync": { "version": "0.1.3", - "from": "findup-sync@~0.1.2", + "from": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", "dependencies": { "lodash": { "version": "2.4.2", - "from": "lodash@~2.4.1", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, "isbinaryfile": { "version": "0.1.9", - "from": "isbinaryfile@~0.1.9", + "from": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz" } } }, "lodash": { "version": "2.4.2", - "from": "lodash@~2.4.1", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, "async": { "version": "0.6.2", - "from": "async@0.6.2", + "from": "https://registry.npmjs.org/async/-/async-0.6.2.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.6.2.tgz" }, "base64-stream": { "version": "0.1.3", - "from": "base64-stream@^0.1.2", + "from": "https://registry.npmjs.org/base64-stream/-/base64-stream-0.1.3.tgz", "resolved": "https://registry.npmjs.org/base64-stream/-/base64-stream-0.1.3.tgz", "dependencies": { "readable-stream": { "version": "2.1.4", - "from": "readable-stream@^2.0.2", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", - "from": "buffer-shims@^1.0.0", + "from": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" }, "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "isarray": { "version": "1.0.0", - "from": "isarray@~1.0.0", + "from": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "process-nextick-args": { "version": "1.0.7", - "from": "process-nextick-args@~1.0.6", + "from": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@~1.0.1", + "from": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } @@ -247,35 +247,35 @@ }, "basic-auth-connect": { "version": "1.0.0", - "from": "basic-auth-connect@^1.0.0", + "from": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz" }, "bcrypt": { "version": "0.8.3", - "from": "bcrypt@0.8.3", + "from": "https://registry.npmjs.org/bcrypt/-/bcrypt-0.8.3.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-0.8.3.tgz", "dependencies": { "bindings": { "version": "1.2.1", - "from": "bindings@1.2.1", + "from": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" }, "nan": { "version": "1.8.4", - "from": "nan@1.8.4", + "from": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" } } }, "body-parser": { - "version": "1.15.1", - "from": "body-parser@^1.13.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.1.tgz", + "version": "1.15.2", + "from": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.2.tgz", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.15.2.tgz", "dependencies": { "bytes": { - "version": "2.3.0", - "from": "bytes@2.3.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" + "version": "2.4.0", + "from": "bytes@2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz" }, "content-type": { "version": "1.0.2", @@ -284,41 +284,46 @@ }, "debug": { "version": "2.2.0", - "from": "debug@~2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "depd": { "version": "1.1.0", - "from": "depd@~1.1.0", + "from": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" }, "http-errors": { - "version": "1.4.0", - "from": "http-errors@~1.4.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.4.0.tgz", + "version": "1.5.0", + "from": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, + "setprototypeof": { + "version": "1.0.1", + "from": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz" + }, "statuses": { "version": "1.3.0", - "from": "statuses@>= 1.2.1 < 2", + "from": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" } } }, "iconv-lite": { "version": "0.4.13", - "from": "iconv-lite@0.4.13", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" }, "on-finished": { @@ -328,46 +333,46 @@ "dependencies": { "ee-first": { "version": "1.1.1", - "from": "ee-first@1.1.1", + "from": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" } } }, "qs": { - "version": "6.1.0", - "from": "qs@6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" + "version": "6.2.0", + "from": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz" }, "raw-body": { - "version": "2.1.6", - "from": "raw-body@~2.1.6", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz", + "version": "2.1.7", + "from": "raw-body@~2.1.5", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", "dependencies": { "unpipe": { "version": "1.0.0", - "from": "unpipe@1.0.0", + "from": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" } } }, "type-is": { "version": "1.6.13", - "from": "type-is@~1.6.12", + "from": "type-is@~1.6.10", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz", "dependencies": { "media-typer": { "version": "0.3.0", - "from": "media-typer@0.3.0", + "from": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" }, "mime-types": { "version": "2.1.11", - "from": "mime-types@~2.1.11", + "from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "dependencies": { "mime-db": { "version": "1.23.0", - "from": "mime-db@~1.23.0", + "from": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" } } @@ -378,163 +383,163 @@ }, "bufferedstream": { "version": "1.6.0", - "from": "bufferedstream@1.6.0", + "from": "https://registry.npmjs.org/bufferedstream/-/bufferedstream-1.6.0.tgz", "resolved": "https://registry.npmjs.org/bufferedstream/-/bufferedstream-1.6.0.tgz" }, "connect-redis": { "version": "2.3.0", - "from": "connect-redis@2.3.0", + "from": "https://registry.npmjs.org/connect-redis/-/connect-redis-2.3.0.tgz", "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-2.3.0.tgz", "dependencies": { "debug": { "version": "1.0.4", - "from": "debug@^1.0.4", + "from": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", "dependencies": { "ms": { "version": "0.6.2", - "from": "ms@0.6.2", + "from": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz" } } }, "redis": { "version": "0.12.1", - "from": "redis@^0.12.1", + "from": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz" } } }, "contentful": { - "version": "3.3.14", - "from": "contentful@^3.3.14", - "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.3.14.tgz", + "version": "3.5.0", + "from": "https://registry.npmjs.org/contentful/-/contentful-3.5.0.tgz", + "resolved": "https://registry.npmjs.org/contentful/-/contentful-3.5.0.tgz", "dependencies": { "babel-runtime": { - "version": "6.9.2", - "from": "babel-runtime@^6.3.19", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.2.tgz", + "version": "6.11.6", + "from": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz", "dependencies": { "core-js": { - "version": "2.4.0", - "from": "core-js@^2.4.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz" + "version": "2.4.1", + "from": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz" }, "regenerator-runtime": { "version": "0.9.5", - "from": "regenerator-runtime@^0.9.5", + "from": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz" } } }, "contentful-sdk-core": { - "version": "2.2.4", - "from": "contentful-sdk-core@^2.2.1", - "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.2.4.tgz", + "version": "2.3.4", + "from": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.3.4.tgz", + "resolved": "https://registry.npmjs.org/contentful-sdk-core/-/contentful-sdk-core-2.3.4.tgz", "dependencies": { "follow-redirects": { "version": "0.0.7", - "from": "follow-redirects@0.0.7", + "from": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", "dependencies": { "debug": { "version": "2.2.0", - "from": "debug@^2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "stream-consume": { "version": "0.1.0", - "from": "stream-consume@^0.1.0", + "from": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz" } } }, "qs": { - "version": "6.2.0", - "from": "qs@^6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.0.tgz" + "version": "6.2.1", + "from": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz" } } }, "json-stringify-safe": { "version": "5.0.1", - "from": "json-stringify-safe@^5.0.1", + "from": "json-stringify-safe@~5.0.0", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" } } }, "cookie": { "version": "0.2.4", - "from": "cookie@^0.2.3", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz" }, "cookie-parser": { "version": "1.3.5", - "from": "cookie-parser@1.3.5", + "from": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", "dependencies": { "cookie": { "version": "0.1.3", - "from": "cookie@0.1.3", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" }, "cookie-signature": { "version": "1.0.6", - "from": "cookie-signature@1.0.6", + "from": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" } } }, "csurf": { "version": "1.9.0", - "from": "csurf@^1.8.3", + "from": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", "dependencies": { "cookie": { "version": "0.3.1", - "from": "cookie@0.3.1", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz" }, "cookie-signature": { "version": "1.0.6", - "from": "cookie-signature@1.0.6", + "from": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" }, "csrf": { "version": "3.0.3", - "from": "csrf@~3.0.3", + "from": "https://registry.npmjs.org/csrf/-/csrf-3.0.3.tgz", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.3.tgz", "dependencies": { "base64-url": { "version": "1.2.2", - "from": "base64-url@1.2.2", + "from": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.2.tgz", "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.2.tgz" }, "rndm": { "version": "1.2.0", - "from": "rndm@1.2.0", + "from": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz" }, "tsscmp": { "version": "1.0.5", - "from": "tsscmp@1.0.5", + "from": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz" }, "uid-safe": { "version": "2.1.1", - "from": "uid-safe@2.1.1", + "from": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.1.tgz", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.1.tgz", "dependencies": { "random-bytes": { "version": "1.0.0", - "from": "random-bytes@~1.0.0", + "from": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz" } } @@ -543,22 +548,22 @@ }, "http-errors": { "version": "1.5.0", - "from": "http-errors@~1.5.0", + "from": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "setprototypeof": { "version": "1.0.1", - "from": "setprototypeof@1.0.1", + "from": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz" }, "statuses": { "version": "1.3.0", - "from": "statuses@>= 1.3.0 < 2", + "from": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" } } @@ -567,46 +572,46 @@ }, "dateformat": { "version": "1.0.4-1.2.3", - "from": "dateformat@1.0.4-1.2.3", + "from": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.4-1.2.3.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.4-1.2.3.tgz" }, "express": { "version": "4.13.0", - "from": "express@4.13.0", + "from": "https://registry.npmjs.org/express/-/express-4.13.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz", "dependencies": { "accepts": { "version": "1.2.13", - "from": "accepts@~1.2.9", + "from": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", "dependencies": { "mime-types": { "version": "2.1.11", - "from": "mime-types@~2.1.11", + "from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "dependencies": { "mime-db": { "version": "1.23.0", - "from": "mime-db@~1.23.0", + "from": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" } } }, "negotiator": { "version": "0.5.3", - "from": "negotiator@0.5.3", + "from": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz" } } }, "array-flatten": { "version": "1.1.0", - "from": "array-flatten@1.1.0", + "from": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.0.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.0.tgz" }, "content-disposition": { "version": "0.5.0", - "from": "content-disposition@0.5.0", + "from": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz" }, "content-type": { @@ -616,12 +621,12 @@ }, "cookie": { "version": "0.1.3", - "from": "cookie@0.1.3", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" }, "cookie-signature": { "version": "1.0.6", - "from": "cookie-signature@1.0.6", + "from": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" }, "debug": { @@ -631,51 +636,51 @@ "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "depd": { "version": "1.0.1", - "from": "depd@~1.0.1", + "from": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz" }, "escape-html": { "version": "1.0.2", - "from": "escape-html@1.0.2", + "from": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz" }, "etag": { "version": "1.7.0", - "from": "etag@~1.7.0", + "from": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz" }, "finalhandler": { "version": "0.4.0", - "from": "finalhandler@0.4.0", + "from": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", "dependencies": { "unpipe": { "version": "1.0.0", - "from": "unpipe@~1.0.0", + "from": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" } } }, "fresh": { "version": "0.3.0", - "from": "fresh@0.3.0", + "from": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz" }, "merge-descriptors": { "version": "1.0.0", - "from": "merge-descriptors@1.0.0", + "from": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz" }, "methods": { "version": "1.1.2", - "from": "methods@~1.1.1", + "from": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" }, "on-finished": { @@ -685,137 +690,137 @@ "dependencies": { "ee-first": { "version": "1.1.1", - "from": "ee-first@1.1.1", + "from": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" } } }, "parseurl": { "version": "1.3.1", - "from": "parseurl@~1.3.0", + "from": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" }, "path-to-regexp": { "version": "0.1.6", - "from": "path-to-regexp@0.1.6", + "from": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.6.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.6.tgz" }, "proxy-addr": { "version": "1.0.10", - "from": "proxy-addr@~1.0.8", + "from": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", "dependencies": { "forwarded": { "version": "0.1.0", - "from": "forwarded@~0.1.0", + "from": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz" }, "ipaddr.js": { "version": "1.0.5", - "from": "ipaddr.js@1.0.5", + "from": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz" } } }, "qs": { "version": "2.4.2", - "from": "qs@2.4.2", + "from": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz" }, "range-parser": { "version": "1.0.3", - "from": "range-parser@~1.0.2", + "from": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz" }, "send": { "version": "0.13.0", - "from": "send@0.13.0", + "from": "https://registry.npmjs.org/send/-/send-0.13.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz", "dependencies": { "destroy": { "version": "1.0.3", - "from": "destroy@1.0.3", + "from": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz" }, "http-errors": { "version": "1.3.1", - "from": "http-errors@~1.3.1", + "from": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "mime": { "version": "1.3.4", - "from": "mime@1.3.4", + "from": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" }, "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" }, "statuses": { "version": "1.2.1", - "from": "statuses@~1.2.1", + "from": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" } } }, "serve-static": { "version": "1.10.3", - "from": "serve-static@~1.10.0", + "from": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz", "dependencies": { "escape-html": { "version": "1.0.3", - "from": "escape-html@~1.0.3", + "from": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" }, "send": { "version": "0.13.2", - "from": "send@0.13.2", + "from": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", "dependencies": { "depd": { "version": "1.1.0", - "from": "depd@~1.1.0", + "from": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" }, "destroy": { "version": "1.0.4", - "from": "destroy@~1.0.4", + "from": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" }, "http-errors": { "version": "1.3.1", - "from": "http-errors@~1.3.1", + "from": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "mime": { "version": "1.3.4", - "from": "mime@1.3.4", + "from": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" }, "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" }, "statuses": { "version": "1.2.1", - "from": "statuses@~1.2.1", + "from": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" } } @@ -824,22 +829,22 @@ }, "type-is": { "version": "1.6.13", - "from": "type-is@~1.6.3", + "from": "type-is@~1.6.10", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz", "dependencies": { "media-typer": { "version": "0.3.0", - "from": "media-typer@0.3.0", + "from": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" }, "mime-types": { "version": "2.1.11", - "from": "mime-types@~2.1.11", + "from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "dependencies": { "mime-db": { "version": "1.23.0", - "from": "mime-db@~1.23.0", + "from": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" } } @@ -848,34 +853,34 @@ }, "vary": { "version": "1.0.1", - "from": "vary@~1.0.0", + "from": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz" }, "utils-merge": { "version": "1.0.0", - "from": "utils-merge@1.0.0", + "from": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" } } }, "express-session": { "version": "1.11.3", - "from": "express-session@1.11.3", + "from": "https://registry.npmjs.org/express-session/-/express-session-1.11.3.tgz", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.11.3.tgz", "dependencies": { "cookie": { "version": "0.1.3", - "from": "cookie@0.1.3", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" }, "cookie-signature": { "version": "1.0.6", - "from": "cookie-signature@1.0.6", + "from": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" }, "crc": { "version": "3.3.0", - "from": "crc@3.3.0", + "from": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz", "resolved": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz" }, "debug": { @@ -885,19 +890,19 @@ "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "depd": { "version": "1.0.1", - "from": "depd@~1.0.1", + "from": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz" }, "on-headers": { "version": "1.0.1", - "from": "on-headers@~1.0.0", + "from": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz" }, "parseurl": { @@ -907,81 +912,81 @@ }, "uid-safe": { "version": "2.0.0", - "from": "uid-safe@~2.0.0", + "from": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz", "dependencies": { "base64-url": { "version": "1.2.1", - "from": "base64-url@1.2.1", + "from": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz", "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz" } } }, "utils-merge": { "version": "1.0.0", - "from": "utils-merge@1.0.0", + "from": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" } } }, "grunt": { "version": "0.4.5", - "from": "grunt@^0.4.5", + "from": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", "dependencies": { "async": { "version": "0.1.22", - "from": "async@~0.1.22", + "from": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" }, "coffee-script": { "version": "1.3.3", - "from": "coffee-script@~1.3.3", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz" }, "colors": { "version": "0.6.2", - "from": "colors@~0.6.2", + "from": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz" }, "dateformat": { "version": "1.0.2-1.2.3", - "from": "dateformat@1.0.2-1.2.3", + "from": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" }, "eventemitter2": { "version": "0.4.14", - "from": "eventemitter2@~0.4.13", + "from": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" }, "findup-sync": { "version": "0.1.3", - "from": "findup-sync@~0.1.2", + "from": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", "dependencies": { "glob": { "version": "3.2.11", - "from": "glob@~3.2.9", + "from": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { "version": "0.3.0", - "from": "minimatch@0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } @@ -990,149 +995,149 @@ }, "lodash": { "version": "2.4.2", - "from": "lodash@~2.4.1", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, "glob": { "version": "3.1.21", - "from": "glob@~3.1.21", + "from": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", "dependencies": { "graceful-fs": { "version": "1.2.3", - "from": "graceful-fs@~1.2.0", + "from": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" }, "inherits": { "version": "1.0.2", - "from": "inherits@1", + "from": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" } } }, "hooker": { "version": "0.2.3", - "from": "hooker@~0.2.3", + "from": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" }, "iconv-lite": { "version": "0.2.11", - "from": "iconv-lite@~0.2.11", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" }, "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.12", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } }, "nopt": { "version": "1.0.10", - "from": "nopt@~1.0.10", + "from": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", "dependencies": { "abbrev": { - "version": "1.0.7", - "from": "abbrev@1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" + "version": "1.0.9", + "from": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz" } } }, "rimraf": { "version": "2.2.8", - "from": "rimraf@~2.2.8", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" }, "lodash": { "version": "0.9.2", - "from": "lodash@~0.9.2", + "from": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" }, "underscore.string": { "version": "2.2.1", - "from": "underscore.string@~2.2.1", + "from": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz" }, "which": { "version": "1.0.9", - "from": "which@~1.0.5", + "from": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" }, "js-yaml": { "version": "2.0.5", - "from": "js-yaml@~2.0.5", + "from": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", "dependencies": { "argparse": { "version": "0.1.16", - "from": "argparse@~ 0.1.11", + "from": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", "dependencies": { "underscore": { "version": "1.7.0", - "from": "underscore@~1.7.0", + "from": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" }, "underscore.string": { "version": "2.4.0", - "from": "underscore.string@~2.4.0", + "from": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" } } }, "esprima": { "version": "1.0.4", - "from": "esprima@~ 1.0.2", + "from": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" } } }, "exit": { "version": "0.1.2", - "from": "exit@~0.1.1", + "from": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" }, "getobject": { "version": "0.1.0", - "from": "getobject@~0.1.0", + "from": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz" }, "grunt-legacy-util": { "version": "0.2.0", - "from": "grunt-legacy-util@~0.2.0", + "from": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz" }, "grunt-legacy-log": { "version": "0.1.3", - "from": "grunt-legacy-log@~0.1.0", + "from": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", "dependencies": { "grunt-legacy-log-utils": { "version": "0.1.1", - "from": "grunt-legacy-log-utils@~0.1.1", + "from": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz" }, "lodash": { "version": "2.4.2", - "from": "lodash@~2.4.1", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" }, "underscore.string": { "version": "2.3.3", - "from": "underscore.string@~2.3.3", + "from": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" } } @@ -1141,100 +1146,100 @@ }, "heapdump": { "version": "0.3.7", - "from": "heapdump@^0.3.7", + "from": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.7.tgz", "resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.7.tgz" }, "http-proxy": { - "version": "1.13.3", - "from": "http-proxy@^1.8.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.13.3.tgz", + "version": "1.14.0", + "from": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.14.0.tgz", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.14.0.tgz", "dependencies": { "eventemitter3": { "version": "1.2.0", - "from": "eventemitter3@1.x.x", + "from": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz" }, "requires-port": { "version": "1.0.0", - "from": "requires-port@1.x.x", + "from": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" } } }, "jade": { "version": "1.3.1", - "from": "jade@~1.3.1", + "from": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz", "resolved": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz", "dependencies": { "commander": { "version": "2.1.0", - "from": "commander@2.1.0", + "from": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz" }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "transformers": { "version": "2.1.0", - "from": "transformers@2.1.0", + "from": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", "dependencies": { "promise": { "version": "2.0.0", - "from": "promise@~2.0", + "from": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", "dependencies": { "is-promise": { "version": "1.0.1", - "from": "is-promise@~1", + "from": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz" } } }, "css": { "version": "1.0.8", - "from": "css@~1.0.8", + "from": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", "dependencies": { "css-parse": { "version": "1.0.4", - "from": "css-parse@1.0.4", + "from": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz" }, "css-stringify": { "version": "1.0.5", - "from": "css-stringify@1.0.5", + "from": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz" } } }, "uglify-js": { "version": "2.2.5", - "from": "uglify-js@~2.2.5", + "from": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", "dependencies": { "source-map": { "version": "0.1.43", - "from": "source-map@~0.1.7", + "from": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", "dependencies": { "amdefine": { "version": "1.0.0", - "from": "amdefine@>=0.0.4", + "from": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" } } }, "optimist": { "version": "0.3.7", - "from": "optimist@~0.3.5", + "from": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", "dependencies": { "wordwrap": { "version": "0.0.3", - "from": "wordwrap@~0.0.2", + "from": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" } } @@ -1245,37 +1250,37 @@ }, "character-parser": { "version": "1.2.0", - "from": "character-parser@1.2.0", + "from": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.0.tgz", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.0.tgz" }, "monocle": { "version": "1.1.51", - "from": "monocle@1.1.51", + "from": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz", "resolved": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz", "dependencies": { "readdirp": { "version": "0.2.5", - "from": "readdirp@~0.2.3", + "from": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-0.2.5.tgz", "dependencies": { "minimatch": { - "version": "3.0.0", - "from": "minimatch@>=0.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -1288,59 +1293,59 @@ }, "with": { "version": "3.0.1", - "from": "with@~3.0.0", + "from": "https://registry.npmjs.org/with/-/with-3.0.1.tgz", "resolved": "https://registry.npmjs.org/with/-/with-3.0.1.tgz", "dependencies": { "uglify-js": { "version": "2.4.24", - "from": "uglify-js@~2.4.12", + "from": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", "dependencies": { "async": { "version": "0.2.10", - "from": "async@~0.2.6", + "from": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" }, "source-map": { "version": "0.1.34", - "from": "source-map@0.1.34", + "from": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", "dependencies": { "amdefine": { "version": "1.0.0", - "from": "amdefine@>=0.0.4", + "from": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" } } }, "uglify-to-browserify": { "version": "1.0.2", - "from": "uglify-to-browserify@~1.0.0", + "from": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" }, "yargs": { "version": "3.5.4", - "from": "yargs@~3.5.4", + "from": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", "dependencies": { "camelcase": { "version": "1.2.1", - "from": "camelcase@^1.0.2", + "from": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" }, "decamelize": { "version": "1.2.0", - "from": "decamelize@^1.0.0", + "from": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" }, "window-size": { "version": "0.1.0", - "from": "window-size@0.1.0", + "from": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" }, "wordwrap": { "version": "0.0.2", - "from": "wordwrap@0.0.2", + "from": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" } } @@ -1351,59 +1356,59 @@ }, "constantinople": { "version": "2.0.1", - "from": "constantinople@~2.0.0", + "from": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz", "dependencies": { "uglify-js": { "version": "2.4.24", - "from": "uglify-js@~2.4.0", + "from": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", "dependencies": { "async": { "version": "0.2.10", - "from": "async@~0.2.6", + "from": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" }, "source-map": { "version": "0.1.34", - "from": "source-map@0.1.34", + "from": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", "dependencies": { "amdefine": { "version": "1.0.0", - "from": "amdefine@>=0.0.4", + "from": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" } } }, "uglify-to-browserify": { "version": "1.0.2", - "from": "uglify-to-browserify@~1.0.0", + "from": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" }, "yargs": { "version": "3.5.4", - "from": "yargs@~3.5.4", + "from": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", "dependencies": { "camelcase": { "version": "1.2.1", - "from": "camelcase@^1.0.2", + "from": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" }, "decamelize": { "version": "1.2.0", - "from": "decamelize@^1.0.0", + "from": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" }, "window-size": { "version": "0.1.0", - "from": "window-size@0.1.0", + "from": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" }, "wordwrap": { "version": "0.0.2", - "from": "wordwrap@0.0.2", + "from": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" } } @@ -1416,91 +1421,91 @@ }, "ldapjs": { "version": "1.0.0", - "from": "ldapjs@^1.0.0", + "from": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.0.tgz", "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.0.tgz", "dependencies": { "asn1": { "version": "0.2.3", - "from": "asn1@0.2.3", + "from": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" }, "assert-plus": { "version": "0.1.5", - "from": "assert-plus@0.1.5", + "from": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" }, "bunyan": { "version": "1.5.1", - "from": "bunyan@1.5.1", + "from": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", "dependencies": { "mv": { "version": "2.1.1", - "from": "mv@~2", + "from": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@~0.5.1", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } } }, "ncp": { "version": "2.0.0", - "from": "ncp@~2.0.0", + "from": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" }, "rimraf": { "version": "2.4.5", - "from": "rimraf@~2.4.0", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "dependencies": { "glob": { "version": "6.0.4", - "from": "glob@^6.0.1", + "from": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "dependencies": { "inflight": { "version": "1.0.5", - "from": "inflight@^1.0.4", + "from": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -1509,7 +1514,7 @@ }, "path-is-absolute": { "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", + "from": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" } } @@ -1520,170 +1525,170 @@ }, "safe-json-stringify": { "version": "1.0.3", - "from": "safe-json-stringify@~1", + "from": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz" } } }, "dashdash": { "version": "1.10.1", - "from": "dashdash@1.10.1", + "from": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz" }, "backoff": { "version": "2.4.1", - "from": "backoff@2.4.1", + "from": "https://registry.npmjs.org/backoff/-/backoff-2.4.1.tgz", "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.4.1.tgz", "dependencies": { "precond": { "version": "0.2.3", - "from": "precond@0.2", + "from": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz" } } }, "ldap-filter": { "version": "0.2.2", - "from": "ldap-filter@0.2.2", + "from": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz", "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz" }, "once": { "version": "1.3.2", - "from": "once@1.3.2", + "from": "https://registry.npmjs.org/once/-/once-1.3.2.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.2.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "vasync": { "version": "1.6.3", - "from": "vasync@1.6.3", + "from": "https://registry.npmjs.org/vasync/-/vasync-1.6.3.tgz", "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.3.tgz" }, "verror": { "version": "1.6.0", - "from": "verror@1.6.0", + "from": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", "dependencies": { "extsprintf": { "version": "1.2.0", - "from": "extsprintf@1.2.0", + "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz" } } }, "dtrace-provider": { "version": "0.6.0", - "from": "dtrace-provider@~0.6", + "from": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", "dependencies": { "nan": { - "version": "2.3.5", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz" + "version": "2.4.0", + "from": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" } } } } }, "lodash": { - "version": "4.13.1", - "from": "lodash@^4.13.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz" + "version": "4.14.2", + "from": "https://registry.npmjs.org/lodash/-/lodash-4.14.2.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.14.2.tgz" }, "logger-sharelatex": { "version": "1.3.1", - "from": "logger-sharelatex@git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1", + "from": "logger-sharelatex@git+https://github.com/sharelatex/logger-sharelatex.git#bf413ec621a000cf0e08c939de38d5e24541a08c", "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#bf413ec621a000cf0e08c939de38d5e24541a08c", "dependencies": { "bunyan": { "version": "1.5.1", - "from": "bunyan@1.5.1", + "from": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", "dependencies": { "dtrace-provider": { "version": "0.6.0", - "from": "dtrace-provider@~0.6", + "from": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", "dependencies": { "nan": { - "version": "2.3.5", - "from": "nan@^2.0.8", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz" + "version": "2.4.0", + "from": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" } } }, "mv": { "version": "2.1.1", - "from": "mv@~2", + "from": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@~0.5.1", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } } }, "ncp": { "version": "2.0.0", - "from": "ncp@~2.0.0", + "from": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" }, "rimraf": { "version": "2.4.5", - "from": "rimraf@~2.4.0", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "dependencies": { "glob": { "version": "6.0.4", - "from": "glob@^6.0.1", + "from": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "dependencies": { "inflight": { "version": "1.0.5", - "from": "inflight@^1.0.4", + "from": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -1692,19 +1697,19 @@ }, "once": { "version": "1.3.3", - "from": "once@^1.3.0", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "path-is-absolute": { "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", + "from": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" } } @@ -1715,34 +1720,34 @@ }, "safe-json-stringify": { "version": "1.0.3", - "from": "safe-json-stringify@~1", + "from": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz" } } }, "coffee-script": { "version": "1.4.0", - "from": "coffee-script@1.4.0", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz" }, "raven": { "version": "0.8.1", - "from": "raven@^0.8.0", + "from": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz", "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz", "dependencies": { "cookie": { "version": "0.1.0", - "from": "cookie@0.1.0", + "from": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" }, "lsmod": { "version": "0.0.3", - "from": "lsmod@~0.0.3", + "from": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz", "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz" }, "stack-trace": { "version": "0.0.7", - "from": "stack-trace@0.0.7", + "from": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz" } } @@ -1751,180 +1756,180 @@ }, "lynx": { "version": "0.1.1", - "from": "lynx@0.1.1", + "from": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", "dependencies": { "mersenne": { "version": "0.0.3", - "from": "mersenne@~0.0.3", + "from": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz", "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz" }, "statsd-parser": { "version": "0.0.4", - "from": "statsd-parser@~0.0.4", + "from": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz", "resolved": "https://registry.npmjs.org/statsd-parser/-/statsd-parser-0.0.4.tgz" } } }, "marked": { - "version": "0.3.5", - "from": "marked@^0.3.5", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz" + "version": "0.3.6", + "from": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz" }, "method-override": { "version": "2.3.6", - "from": "method-override@^2.3.3", + "from": "https://registry.npmjs.org/method-override/-/method-override-2.3.6.tgz", "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.6.tgz", "dependencies": { "debug": { "version": "2.2.0", - "from": "debug@~2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "methods": { "version": "1.1.2", - "from": "methods@~1.1.2", + "from": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" }, "parseurl": { "version": "1.3.1", - "from": "parseurl@~1.3.1", + "from": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" }, "vary": { "version": "1.1.0", - "from": "vary@~1.1.0", + "from": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.0.tgz" } } }, "metrics-sharelatex": { "version": "1.3.0", - "from": "metrics-sharelatex@git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0", + "from": "metrics-sharelatex@git+https://github.com/sharelatex/metrics-sharelatex.git#080c4aeb696edcd5d6d86f202f2c528f0661d7a6", "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#080c4aeb696edcd5d6d86f202f2c528f0661d7a6", "dependencies": { "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" } } }, "mimelib": { "version": "0.2.14", - "from": "mimelib@0.2.14", + "from": "https://registry.npmjs.org/mimelib/-/mimelib-0.2.14.tgz", "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.2.14.tgz", "dependencies": { "encoding": { "version": "0.1.12", - "from": "encoding@~0.1", + "from": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "dependencies": { "iconv-lite": { "version": "0.4.13", - "from": "iconv-lite@~0.4.13", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" } } }, "addressparser": { "version": "0.2.1", - "from": "addressparser@~0.2.0", + "from": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz", "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz" } } }, "mocha": { "version": "1.17.1", - "from": "mocha@1.17.1", + "from": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", "dependencies": { "commander": { "version": "2.0.0", - "from": "commander@2.0.0", + "from": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" }, "growl": { "version": "1.7.0", - "from": "growl@1.7.x", + "from": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz" }, "jade": { "version": "0.26.3", - "from": "jade@0.26.3", + "from": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "dependencies": { "commander": { "version": "0.6.1", - "from": "commander@0.6.1", + "from": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" }, "mkdirp": { "version": "0.3.0", - "from": "mkdirp@0.3.0", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" } } }, "diff": { "version": "1.0.7", - "from": "diff@1.0.7", + "from": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" }, "debug": { "version": "2.2.0", - "from": "debug@*", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "glob": { "version": "3.2.3", - "from": "glob@3.2.3", + "from": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "dependencies": { "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.11", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } }, "graceful-fs": { "version": "2.0.3", - "from": "graceful-fs@~2.0.0", + "from": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -1933,108 +1938,108 @@ }, "mongojs": { "version": "0.18.2", - "from": "mongojs@0.18.2", + "from": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz", "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz", "dependencies": { "thunky": { "version": "0.1.0", - "from": "thunky@~0.1.0", + "from": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz", "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz" }, "readable-stream": { "version": "1.1.14", - "from": "readable-stream@1.1.x", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "mongodb": { "version": "1.4.32", - "from": "mongodb@1.4.32", + "from": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz", "dependencies": { "bson": { "version": "0.2.22", - "from": "bson@~0.2", + "from": "https://registry.npmjs.org/bson/-/bson-0.2.22.tgz", "resolved": "https://registry.npmjs.org/bson/-/bson-0.2.22.tgz", "dependencies": { "nan": { "version": "1.8.4", - "from": "nan@~1.8", + "from": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" } } }, "kerberos": { "version": "0.0.9", - "from": "kerberos@0.0.9", + "from": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz", "dependencies": { "nan": { "version": "1.6.2", - "from": "nan@1.6.2", + "from": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz" } } }, "readable-stream": { "version": "2.1.4", - "from": "readable-stream@latest", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", - "from": "buffer-shims@^1.0.0", + "from": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" }, "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "isarray": { "version": "1.0.0", - "from": "isarray@~1.0.0", + "from": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "process-nextick-args": { "version": "1.0.7", - "from": "process-nextick-args@~1.0.6", + "from": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@~1.0.1", + "from": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } @@ -2045,32 +2050,32 @@ }, "mongoose": { "version": "4.1.0", - "from": "mongoose@4.1.0", + "from": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz", "dependencies": { "async": { "version": "0.9.0", - "from": "async@0.9.0", + "from": "https://registry.npmjs.org/async/-/async-0.9.0.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" }, "bson": { "version": "0.3.2", - "from": "bson@~0.3", + "from": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz", "resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz", "dependencies": { "bson-ext": { "version": "0.1.13", - "from": "bson-ext@~0.1", + "from": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz", "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz", "dependencies": { "bindings": { "version": "1.2.1", - "from": "bindings@^1.2.1", + "from": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" }, "nan": { "version": "2.0.9", - "from": "nan@~2.0.9", + "from": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz" } } @@ -2079,37 +2084,37 @@ }, "hooks-fixed": { "version": "1.1.0", - "from": "hooks-fixed@1.1.0", + "from": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz", "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz" }, "kareem": { "version": "1.0.1", - "from": "kareem@1.0.1", + "from": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz", "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz" }, "mongodb": { "version": "2.0.34", - "from": "mongodb@2.0.34", + "from": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz", "dependencies": { "mongodb-core": { "version": "1.2.0", - "from": "mongodb-core@1.2.0", + "from": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz", "dependencies": { "bson": { "version": "0.4.23", - "from": "bson@~0.4", + "from": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz", "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz" }, "kerberos": { "version": "0.0.21", - "from": "kerberos@~0.0", + "from": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.21.tgz", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.21.tgz", "dependencies": { "nan": { "version": "2.3.5", - "from": "nan@~2.3", + "from": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz" } } @@ -2118,27 +2123,27 @@ }, "readable-stream": { "version": "1.0.31", - "from": "readable-stream@1.0.31", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -2147,32 +2152,32 @@ }, "mpath": { "version": "0.1.1", - "from": "mpath@0.1.1", + "from": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz" }, "mpromise": { "version": "0.5.4", - "from": "mpromise@0.5.4", + "from": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz", "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz" }, "mquery": { "version": "1.6.1", - "from": "mquery@1.6.1", + "from": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz", "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz", "dependencies": { "bluebird": { "version": "2.9.26", - "from": "bluebird@2.9.26", + "from": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz" }, "debug": { "version": "2.2.0", - "from": "debug@2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } @@ -2181,71 +2186,71 @@ }, "ms": { "version": "0.1.0", - "from": "ms@0.1.0", + "from": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz" }, "muri": { "version": "1.0.0", - "from": "muri@1.0.0", + "from": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz", "resolved": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz" }, "regexp-clone": { "version": "0.0.1", - "from": "regexp-clone@0.0.1", + "from": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz" }, "sliced": { "version": "0.0.5", - "from": "sliced@0.0.5", + "from": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz" } } }, "multer": { "version": "0.1.8", - "from": "multer@^0.1.8", + "from": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz", "dependencies": { "busboy": { "version": "0.2.13", - "from": "busboy@~0.2.9", + "from": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz", "dependencies": { "dicer": { "version": "0.2.5", - "from": "dicer@0.2.5", + "from": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", "dependencies": { "streamsearch": { "version": "0.1.2", - "from": "streamsearch@0.1.2", + "from": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" } } }, "readable-stream": { "version": "1.1.14", - "from": "readable-stream@1.1.x", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", + "from": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -2254,32 +2259,32 @@ }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "qs": { "version": "1.2.2", - "from": "qs@~1.2.2", + "from": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz" }, "type-is": { "version": "1.5.7", - "from": "type-is@~1.5.2", + "from": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", "dependencies": { "media-typer": { "version": "0.3.0", - "from": "media-typer@0.3.0", + "from": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" }, "mime-types": { "version": "2.0.14", - "from": "mime-types@~2.0.9", + "from": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", "dependencies": { "mime-db": { "version": "1.12.0", - "from": "mime-db@~1.12.0", + "from": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" } } @@ -2290,64 +2295,64 @@ }, "node-uuid": { "version": "1.4.1", - "from": "node-uuid@1.4.1", + "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.1.tgz", "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.1.tgz" }, "nodemailer": { "version": "2.1.0", - "from": "nodemailer@2.1.0", + "from": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz", "dependencies": { "libmime": { "version": "2.0.0", - "from": "libmime@2.0.0", + "from": "https://registry.npmjs.org/libmime/-/libmime-2.0.0.tgz", "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.0.0.tgz", "dependencies": { "iconv-lite": { "version": "0.4.13", - "from": "iconv-lite@0.4.13", + "from": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" }, "libbase64": { "version": "0.1.0", - "from": "libbase64@0.1.0", + "from": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz" }, "libqp": { "version": "1.1.0", - "from": "libqp@1.1.0", + "from": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz" } } }, "mailcomposer": { "version": "3.3.2", - "from": "mailcomposer@3.3.2", + "from": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.3.2.tgz", "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.3.2.tgz", "dependencies": { "buildmail": { "version": "3.3.2", - "from": "buildmail@3.3.2", + "from": "https://registry.npmjs.org/buildmail/-/buildmail-3.3.2.tgz", "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-3.3.2.tgz", "dependencies": { "addressparser": { "version": "1.0.0", - "from": "addressparser@1.0.0", + "from": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.0.tgz", "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.0.tgz" }, "libbase64": { "version": "0.1.0", - "from": "libbase64@0.1.0", + "from": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz" }, "libqp": { "version": "1.1.0", - "from": "libqp@1.1.0", + "from": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz" }, "nodemailer-fetch": { "version": "1.2.1", - "from": "nodemailer-fetch@1.2.1", + "from": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz" } } @@ -2356,68 +2361,68 @@ }, "nodemailer-direct-transport": { "version": "2.0.1", - "from": "nodemailer-direct-transport@2.0.1", + "from": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-2.0.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-2.0.1.tgz", "dependencies": { "smtp-connection": { "version": "2.0.1", - "from": "smtp-connection@2.0.1", + "from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz", "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz" } } }, "nodemailer-shared": { "version": "1.0.3", - "from": "nodemailer-shared@1.0.3", + "from": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.0.3.tgz", "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.0.3.tgz", "dependencies": { "nodemailer-fetch": { "version": "1.2.1", - "from": "nodemailer-fetch@1.2.1", + "from": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz" } } }, "nodemailer-smtp-pool": { "version": "2.1.0", - "from": "nodemailer-smtp-pool@2.1.0", + "from": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.1.0.tgz", "resolved": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.1.0.tgz", "dependencies": { "clone": { "version": "1.0.2", - "from": "clone@1.0.2", + "from": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" }, "nodemailer-wellknown": { "version": "0.1.7", - "from": "nodemailer-wellknown@0.1.7", + "from": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz", "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz" }, "smtp-connection": { "version": "2.0.1", - "from": "smtp-connection@2.0.1", + "from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz", "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz" } } }, "nodemailer-smtp-transport": { "version": "2.0.1", - "from": "nodemailer-smtp-transport@2.0.1", + "from": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.0.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.0.1.tgz", "dependencies": { "clone": { "version": "1.0.2", - "from": "clone@1.0.2", + "from": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" }, "nodemailer-wellknown": { "version": "0.1.7", - "from": "nodemailer-wellknown@0.1.7", + "from": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz", "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.7.tgz" }, "smtp-connection": { "version": "2.0.1", - "from": "smtp-connection@2.0.1", + "from": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz", "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz" } } @@ -2426,27 +2431,27 @@ }, "nodemailer-sendgrid-transport": { "version": "0.2.0", - "from": "nodemailer-sendgrid-transport@^0.2.0", + "from": "https://registry.npmjs.org/nodemailer-sendgrid-transport/-/nodemailer-sendgrid-transport-0.2.0.tgz", "resolved": "https://registry.npmjs.org/nodemailer-sendgrid-transport/-/nodemailer-sendgrid-transport-0.2.0.tgz", "dependencies": { "sendgrid": { "version": "1.9.2", - "from": "sendgrid@^1.8.0", + "from": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz", "resolved": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz", "dependencies": { "mime": { "version": "1.3.4", - "from": "mime@^1.2.9", + "from": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" }, "lodash": { "version": "3.10.1", - "from": "lodash@^3.0.1 || ^2.0.0", + "from": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" }, "smtpapi": { "version": "1.2.0", - "from": "smtpapi@^1.2.0", + "from": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz", "resolved": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz" } } @@ -2454,40 +2459,40 @@ } }, "nodemailer-ses-transport": { - "version": "1.3.1", - "from": "nodemailer-ses-transport@^1.3.0", - "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.3.1.tgz", + "version": "1.4.0", + "from": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/nodemailer-ses-transport/-/nodemailer-ses-transport-1.4.0.tgz", "dependencies": { "aws-sdk": { - "version": "2.4.0", - "from": "aws-sdk@^2.3.11", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.0.tgz", + "version": "2.4.14", + "from": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.14.tgz", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.4.14.tgz", "dependencies": { "sax": { "version": "1.1.5", - "from": "sax@1.1.5", + "from": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz" }, "xml2js": { "version": "0.4.15", - "from": "xml2js@0.4.15", + "from": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz" }, "xmlbuilder": { "version": "2.6.2", - "from": "xmlbuilder@2.6.2", + "from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.2.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.2.tgz", "dependencies": { "lodash": { "version": "3.5.0", - "from": "lodash@~3.5.0", + "from": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz" } } }, "jmespath": { "version": "0.15.0", - "from": "jmespath@0.15.0", + "from": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz" } } @@ -2496,54 +2501,197 @@ }, "optimist": { "version": "0.6.1", - "from": "optimist@0.6.1", + "from": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "dependencies": { "wordwrap": { "version": "0.0.3", - "from": "wordwrap@~0.0.2", + "from": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" }, "minimist": { "version": "0.0.10", - "from": "minimist@~0.0.1", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" } } }, + "passport": { + "version": "0.3.2", + "from": "passport@*", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz", + "dependencies": { + "passport-strategy": { + "version": "1.0.0", + "from": "passport-strategy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" + }, + "pause": { + "version": "0.0.1", + "from": "pause@0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" + } + } + }, + "passport-local": { + "version": "1.0.0", + "from": "passport-local@*", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "dependencies": { + "passport-strategy": { + "version": "1.0.0", + "from": "passport-strategy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" + } + } + }, + "pg": { + "version": "6.0.3", + "from": "https://registry.npmjs.org/pg/-/pg-6.0.3.tgz", + "resolved": "https://registry.npmjs.org/pg/-/pg-6.0.3.tgz", + "dependencies": { + "buffer-writer": { + "version": "1.0.1", + "from": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz" + }, + "packet-reader": { + "version": "0.2.0", + "from": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.2.0.tgz" + }, + "pg-connection-string": { + "version": "0.1.3", + "from": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz" + }, + "pg-pool": { + "version": "1.4.0", + "from": "https://registry.npmjs.org/pg-pool/-/pg-pool-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-1.4.0.tgz", + "dependencies": { + "generic-pool": { + "version": "2.4.2", + "from": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz" + }, + "object-assign": { + "version": "4.1.0", + "from": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz" + } + } + }, + "pg-types": { + "version": "1.11.0", + "from": "https://registry.npmjs.org/pg-types/-/pg-types-1.11.0.tgz", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.11.0.tgz", + "dependencies": { + "ap": { + "version": "0.2.0", + "from": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz" + }, + "postgres-array": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.0.tgz" + }, + "postgres-bytea": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz" + }, + "postgres-date": { + "version": "1.0.3", + "from": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz" + }, + "postgres-interval": { + "version": "1.0.2", + "from": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.0.2.tgz", + "dependencies": { + "xtend": { + "version": "4.0.1", + "from": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + } + } + }, + "pgpass": { + "version": "0.0.6", + "from": "https://registry.npmjs.org/pgpass/-/pgpass-0.0.6.tgz", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-0.0.6.tgz", + "dependencies": { + "split": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/split/-/split-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz", + "dependencies": { + "through": { + "version": "2.3.8", + "from": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + } + } + } + } + }, + "semver": { + "version": "4.3.2", + "from": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz" + } + } + }, + "pg-hstore": { + "version": "2.3.2", + "from": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.2.tgz", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.2.tgz", + "dependencies": { + "underscore": { + "version": "1.8.3", + "from": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz" + } + } + }, "redback": { "version": "0.4.0", - "from": "redback@0.4.0", + "from": "https://registry.npmjs.org/redback/-/redback-0.4.0.tgz", "resolved": "https://registry.npmjs.org/redback/-/redback-0.4.0.tgz" }, "redis": { "version": "0.10.1", - "from": "redis@0.10.1", + "from": "https://registry.npmjs.org/redis/-/redis-0.10.1.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-0.10.1.tgz" }, "redis-sharelatex": { "version": "0.0.9", - "from": "redis-sharelatex@0.0.9", + "from": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz", "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz", "dependencies": { "chai": { "version": "1.9.1", - "from": "chai@1.9.1", + "from": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz", "dependencies": { "assertion-error": { "version": "1.0.0", - "from": "assertion-error@1.0.0", + "from": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz" }, "deep-eql": { "version": "0.1.3", - "from": "deep-eql@0.1.3", + "from": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", "dependencies": { "type-detect": { "version": "0.1.1", - "from": "type-detect@0.1.1", + "from": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" } } @@ -2552,168 +2700,173 @@ }, "coffee-script": { "version": "1.8.0", - "from": "coffee-script@1.8.0", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz", "dependencies": { "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" } } }, "grunt-contrib-coffee": { "version": "0.11.1", - "from": "grunt-contrib-coffee@0.11.1", + "from": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", "resolved": "https://registry.npmjs.org/grunt-contrib-coffee/-/grunt-contrib-coffee-0.11.1.tgz", "dependencies": { "coffee-script": { "version": "1.7.1", - "from": "coffee-script@~1.7.0", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", "dependencies": { "mkdirp": { "version": "0.3.5", - "from": "mkdirp@~0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" } } }, "chalk": { "version": "0.5.1", - "from": "chalk@~0.5.0", + "from": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", "dependencies": { "ansi-styles": { "version": "1.1.0", - "from": "ansi-styles@^1.1.0", + "from": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" }, "escape-string-regexp": { "version": "1.0.5", - "from": "escape-string-regexp@^1.0.0", + "from": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" }, "has-ansi": { "version": "0.1.0", - "from": "has-ansi@^0.1.0", + "from": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz", "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@^0.2.0", + "from": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" } } }, "strip-ansi": { "version": "0.3.0", - "from": "strip-ansi@^0.3.0", + "from": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz", "dependencies": { "ansi-regex": { "version": "0.2.1", - "from": "ansi-regex@^0.2.0", + "from": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" } } }, "supports-color": { "version": "0.2.0", - "from": "supports-color@^0.2.0", + "from": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" } } }, "lodash": { "version": "2.4.2", - "from": "lodash@~2.4.1", + "from": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" } } }, "grunt-mocha-test": { "version": "0.12.0", - "from": "grunt-mocha-test@0.12.0", + "from": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.12.0.tgz", "resolved": "https://registry.npmjs.org/grunt-mocha-test/-/grunt-mocha-test-0.12.0.tgz", "dependencies": { "hooker": { "version": "0.2.3", - "from": "hooker@~0.2.3", + "from": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" }, "fs-extra": { "version": "0.11.1", - "from": "fs-extra@~0.11.1", + "from": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.11.1.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.11.1.tgz", "dependencies": { "ncp": { "version": "0.6.0", - "from": "ncp@^0.6.0", + "from": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz", "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz" }, "mkdirp": { "version": "0.5.1", - "from": "mkdirp@^0.5.0", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } } }, "jsonfile": { "version": "2.3.1", - "from": "jsonfile@^2.0.0", + "from": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.3.1.tgz" }, "rimraf": { - "version": "2.5.2", - "from": "rimraf@^2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", + "version": "2.5.4", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", "dependencies": { "glob": { - "version": "7.0.3", - "from": "glob@^7.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "version": "7.0.5", + "from": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", "dependencies": { + "fs.realpath": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, "inflight": { "version": "1.0.5", - "from": "inflight@^1.0.4", + "from": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -2722,19 +2875,19 @@ }, "once": { "version": "1.3.3", - "from": "once@^1.3.0", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "path-is-absolute": { "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", + "from": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" } } @@ -2747,88 +2900,88 @@ }, "mocha": { "version": "1.21.4", - "from": "mocha@1.21.4", + "from": "https://registry.npmjs.org/mocha/-/mocha-1.21.4.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.21.4.tgz", "dependencies": { "commander": { "version": "2.0.0", - "from": "commander@2.0.0", + "from": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" }, "growl": { "version": "1.8.1", - "from": "growl@1.8.x", + "from": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz", "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz" }, "jade": { "version": "0.26.3", - "from": "jade@0.26.3", + "from": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", "dependencies": { "commander": { "version": "0.6.1", - "from": "commander@0.6.1", + "from": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" }, "mkdirp": { "version": "0.3.0", - "from": "mkdirp@0.3.0", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" } } }, "diff": { "version": "1.0.7", - "from": "diff@1.0.7", + "from": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" }, "debug": { "version": "2.2.0", - "from": "debug@*", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "mkdirp": { "version": "0.3.5", - "from": "mkdirp@0.3.5", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" }, "glob": { "version": "3.2.3", - "from": "glob@3.2.3", + "from": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", "dependencies": { "minimatch": { "version": "0.2.14", - "from": "minimatch@~0.2.11", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", "dependencies": { "lru-cache": { "version": "2.7.3", - "from": "lru-cache@2", + "from": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" }, "sigmund": { "version": "1.0.1", - "from": "sigmund@~1.0.0", + "from": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" } } }, "graceful-fs": { "version": "2.0.3", - "from": "graceful-fs@~2.0.0", + "from": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -2837,68 +2990,68 @@ }, "redis": { "version": "0.12.1", - "from": "redis@0.12.1", + "from": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz" }, "redis-sentinel": { "version": "0.1.1", - "from": "redis-sentinel@0.1.1", + "from": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz", "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz", "dependencies": { "redis": { "version": "0.11.0", - "from": "redis@0.11.x", + "from": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz" }, "q": { "version": "0.9.2", - "from": "q@0.9.2", + "from": "https://registry.npmjs.org/q/-/q-0.9.2.tgz", "resolved": "https://registry.npmjs.org/q/-/q-0.9.2.tgz" } } }, "sandboxed-module": { "version": "1.0.1", - "from": "sandboxed-module@1.0.1", + "from": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sandboxed-module/-/sandboxed-module-1.0.1.tgz", "dependencies": { "require-like": { "version": "0.1.2", - "from": "require-like@0.1.2", + "from": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz" }, "stack-trace": { "version": "0.0.9", - "from": "stack-trace@0.0.9", + "from": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz" } } }, "sinon": { "version": "1.10.3", - "from": "sinon@1.10.3", + "from": "https://registry.npmjs.org/sinon/-/sinon-1.10.3.tgz", "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.10.3.tgz", "dependencies": { "formatio": { "version": "1.0.2", - "from": "formatio@~1.0", + "from": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz", "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz", "dependencies": { "samsam": { "version": "1.1.3", - "from": "samsam@~1.1", + "from": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz" } } }, "util": { "version": "0.10.3", - "from": "util@>=0.10.3 <1", + "from": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } @@ -2907,64 +3060,64 @@ }, "underscore": { "version": "1.7.0", - "from": "underscore@1.7.0", + "from": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" } } }, "request": { - "version": "2.72.0", - "from": "request@^2.69.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.72.0.tgz", + "version": "2.74.0", + "from": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", + "resolved": "https://registry.npmjs.org/request/-/request-2.74.0.tgz", "dependencies": { "aws-sign2": { "version": "0.6.0", - "from": "aws-sign2@~0.6.0", + "from": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" }, "aws4": { "version": "1.4.1", - "from": "aws4@^1.2.1", + "from": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz" }, "bl": { "version": "1.1.2", - "from": "bl@~1.1.2", + "from": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", "dependencies": { "readable-stream": { "version": "2.0.6", - "from": "readable-stream@~2.0.5", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "dependencies": { "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "isarray": { "version": "1.0.0", - "from": "isarray@~1.0.0", + "from": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "process-nextick-args": { "version": "1.0.7", - "from": "process-nextick-args@~1.0.6", + "from": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@~1.0.1", + "from": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } @@ -2973,148 +3126,148 @@ }, "caseless": { "version": "0.11.0", - "from": "caseless@~0.11.0", + "from": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" }, "combined-stream": { "version": "1.0.5", - "from": "combined-stream@~1.0.5", + "from": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "dependencies": { "delayed-stream": { "version": "1.0.0", - "from": "delayed-stream@~1.0.0", + "from": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" } } }, "extend": { "version": "3.0.0", - "from": "extend@~3.0.0", + "from": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" }, "forever-agent": { "version": "0.6.1", - "from": "forever-agent@~0.6.1", + "from": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" }, "form-data": { "version": "1.0.0-rc4", - "from": "form-data@~1.0.0-rc3", + "from": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", "dependencies": { "async": { "version": "1.5.2", - "from": "async@^1.5.2", + "from": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" } } }, "har-validator": { "version": "2.0.6", - "from": "har-validator@~2.0.6", + "from": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", "dependencies": { "chalk": { "version": "1.1.3", - "from": "chalk@^1.1.1", + "from": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "dependencies": { "ansi-styles": { "version": "2.2.1", - "from": "ansi-styles@^2.2.1", + "from": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" }, "escape-string-regexp": { "version": "1.0.5", - "from": "escape-string-regexp@^1.0.2", + "from": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" }, "has-ansi": { "version": "2.0.0", - "from": "has-ansi@^2.0.0", + "from": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "dependencies": { "ansi-regex": { "version": "2.0.0", - "from": "ansi-regex@^2.0.0", + "from": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" } } }, "strip-ansi": { "version": "3.0.1", - "from": "strip-ansi@^3.0.0", + "from": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "dependencies": { "ansi-regex": { "version": "2.0.0", - "from": "ansi-regex@^2.0.0", + "from": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" } } }, "supports-color": { "version": "2.0.0", - "from": "supports-color@^2.0.0", + "from": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" } } }, "commander": { "version": "2.9.0", - "from": "commander@^2.9.0", + "from": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", "dependencies": { "graceful-readlink": { "version": "1.0.1", - "from": "graceful-readlink@>= 1.0.0", + "from": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" } } }, "is-my-json-valid": { "version": "2.13.1", - "from": "is-my-json-valid@^2.12.4", + "from": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", "dependencies": { "generate-function": { "version": "2.0.0", - "from": "generate-function@^2.0.0", + "from": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" }, "generate-object-property": { "version": "1.2.0", - "from": "generate-object-property@^1.1.0", + "from": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", "dependencies": { "is-property": { "version": "1.0.2", - "from": "is-property@^1.0.0", + "from": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" } } }, "jsonpointer": { "version": "2.0.0", - "from": "jsonpointer@2.0.0", + "from": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" }, "xtend": { "version": "4.0.1", - "from": "xtend@^4.0.0", + "from": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" } } }, "pinkie-promise": { "version": "2.0.1", - "from": "pinkie-promise@^2.0.0", + "from": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "dependencies": { "pinkie": { "version": "2.0.4", - "from": "pinkie@^2.0.0", + "from": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" } } @@ -3123,106 +3276,106 @@ }, "hawk": { "version": "3.1.3", - "from": "hawk@~3.1.3", + "from": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "dependencies": { "hoek": { "version": "2.16.3", - "from": "hoek@2.x.x", + "from": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" }, "boom": { "version": "2.10.1", - "from": "boom@2.x.x", + "from": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" }, "cryptiles": { "version": "2.0.5", - "from": "cryptiles@2.x.x", + "from": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" }, "sntp": { "version": "1.0.9", - "from": "sntp@1.x.x", + "from": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" } } }, "http-signature": { "version": "1.1.1", - "from": "http-signature@~1.1.0", + "from": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "dependencies": { "assert-plus": { "version": "0.2.0", - "from": "assert-plus@^0.2.0", + "from": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" }, "jsprim": { - "version": "1.2.2", - "from": "jsprim@^1.2.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz", + "version": "1.3.0", + "from": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.0.tgz", "dependencies": { "extsprintf": { "version": "1.0.2", - "from": "extsprintf@1.0.2", + "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" }, "json-schema": { "version": "0.2.2", - "from": "json-schema@0.2.2", + "from": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" }, "verror": { "version": "1.3.6", - "from": "verror@1.3.6", + "from": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" } } }, "sshpk": { - "version": "1.8.3", - "from": "sshpk@^1.7.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", + "version": "1.9.2", + "from": "https://registry.npmjs.org/sshpk/-/sshpk-1.9.2.tgz", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.9.2.tgz", "dependencies": { "asn1": { "version": "0.2.3", - "from": "asn1@~0.2.3", + "from": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" }, "assert-plus": { "version": "1.0.0", - "from": "assert-plus@^1.0.0", + "from": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" }, "dashdash": { "version": "1.14.0", - "from": "dashdash@^1.12.0", + "from": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz" }, "getpass": { "version": "0.1.6", - "from": "getpass@^0.1.1", + "from": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz" }, "jsbn": { "version": "0.1.0", - "from": "jsbn@~0.1.0", + "from": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" }, "tweetnacl": { "version": "0.13.3", - "from": "tweetnacl@~0.13.0", + "from": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" }, "jodid25519": { "version": "1.0.2", - "from": "jodid25519@^1.0.0", + "from": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" }, "ecc-jsbn": { "version": "0.1.1", - "from": "ecc-jsbn@~0.1.1", + "from": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" } } @@ -3231,45 +3384,45 @@ }, "is-typedarray": { "version": "1.0.0", - "from": "is-typedarray@~1.0.0", + "from": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" }, "isstream": { "version": "0.1.2", - "from": "isstream@~0.1.2", + "from": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" }, "json-stringify-safe": { "version": "5.0.1", - "from": "json-stringify-safe@~5.0.1", + "from": "json-stringify-safe@~5.0.0", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" }, "mime-types": { "version": "2.1.11", - "from": "mime-types@~2.1.7", + "from": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", "dependencies": { "mime-db": { "version": "1.23.0", - "from": "mime-db@~1.23.0", + "from": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" } } }, "node-uuid": { "version": "1.4.7", - "from": "node-uuid@~1.4.7", + "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" }, "oauth-sign": { "version": "0.8.2", - "from": "oauth-sign@~0.8.1", + "from": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" }, "qs": { - "version": "6.1.0", - "from": "qs@~6.1.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" + "version": "6.2.1", + "from": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz" }, "stringstream": { "version": "0.0.5", @@ -3277,375 +3430,495 @@ "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" }, "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@~2.2.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" + "version": "2.3.1", + "from": "tough-cookie@>=0.12.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.1.tgz" }, "tunnel-agent": { "version": "0.4.3", - "from": "tunnel-agent@~0.4.1", + "from": "tunnel-agent@~0.4.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" } } }, "requests": { "version": "0.1.7", - "from": "requests@^0.1.7", + "from": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz", "resolved": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz", "dependencies": { "axo": { "version": "0.0.1", - "from": "axo@0.0.x", + "from": "https://registry.npmjs.org/axo/-/axo-0.0.1.tgz", "resolved": "https://registry.npmjs.org/axo/-/axo-0.0.1.tgz" }, "eventemitter3": { "version": "1.1.1", - "from": "eventemitter3@1.1.x", + "from": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz" }, "extendible": { "version": "0.1.1", - "from": "extendible@0.1.x", + "from": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz", "resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz" }, "hang": { "version": "1.0.0", - "from": "hang@1.0.x", + "from": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz", "resolved": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz" }, "loads": { "version": "0.0.4", - "from": "loads@0.0.x", + "from": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz", "resolved": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz", "dependencies": { "failure": { "version": "1.1.1", - "from": "failure@1.1.x", + "from": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz", "resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz" }, "one-time": { "version": "0.0.4", - "from": "one-time@0.0.x", + "from": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz" }, "xhr-response": { "version": "1.0.1", - "from": "xhr-response@1.0.x", + "from": "https://registry.npmjs.org/xhr-response/-/xhr-response-1.0.1.tgz", "resolved": "https://registry.npmjs.org/xhr-response/-/xhr-response-1.0.1.tgz" }, "xhr-status": { "version": "1.0.0", - "from": "xhr-status@1.0.x", + "from": "https://registry.npmjs.org/xhr-status/-/xhr-status-1.0.0.tgz", "resolved": "https://registry.npmjs.org/xhr-status/-/xhr-status-1.0.0.tgz" } } }, "xhr-send": { "version": "1.0.0", - "from": "xhr-send@1.0.x", + "from": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz", "resolved": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz" } } }, "rimraf": { "version": "2.2.6", - "from": "rimraf@2.2.6", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz" }, "sanitizer": { "version": "0.1.1", - "from": "sanitizer@0.1.1", + "from": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz" }, + "sequelize": { + "version": "3.23.6", + "from": "https://registry.npmjs.org/sequelize/-/sequelize-3.23.6.tgz", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-3.23.6.tgz", + "dependencies": { + "bluebird": { + "version": "3.4.1", + "from": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.1.tgz", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.1.tgz" + }, + "depd": { + "version": "1.1.0", + "from": "depd@~1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" + }, + "dottie": { + "version": "1.1.1", + "from": "https://registry.npmjs.org/dottie/-/dottie-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-1.1.1.tgz" + }, + "generic-pool": { + "version": "2.4.2", + "from": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.2.tgz" + }, + "inflection": { + "version": "1.10.0", + "from": "https://registry.npmjs.org/inflection/-/inflection-1.10.0.tgz", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.10.0.tgz" + }, + "lodash": { + "version": "4.12.0", + "from": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz" + }, + "moment": { + "version": "2.14.1", + "from": "https://registry.npmjs.org/moment/-/moment-2.14.1.tgz", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.14.1.tgz" + }, + "moment-timezone": { + "version": "0.5.5", + "from": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.5.tgz", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.5.tgz" + }, + "node-uuid": { + "version": "1.4.7", + "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "retry-as-promised": { + "version": "2.0.1", + "from": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-2.0.1.tgz", + "dependencies": { + "debug": { + "version": "2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + } + } + }, + "semver": { + "version": "5.3.0", + "from": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz" + }, + "shimmer": { + "version": "1.1.0", + "from": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.1.0.tgz" + }, + "terraformer-wkt-parser": { + "version": "1.1.0", + "from": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/terraformer-wkt-parser/-/terraformer-wkt-parser-1.1.0.tgz", + "dependencies": { + "terraformer": { + "version": "1.0.5", + "from": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.5.tgz", + "resolved": "https://registry.npmjs.org/terraformer/-/terraformer-1.0.5.tgz" + } + } + }, + "toposort-class": { + "version": "1.0.1", + "from": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz" + }, + "validator": { + "version": "5.5.0", + "from": "https://registry.npmjs.org/validator/-/validator-5.5.0.tgz", + "resolved": "https://registry.npmjs.org/validator/-/validator-5.5.0.tgz" + }, + "wkx": { + "version": "0.2.0", + "from": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.2.0.tgz" + } + } + }, "settings-sharelatex": { "version": "1.0.0", - "from": "settings-sharelatex@git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "from": "settings-sharelatex@git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", "dependencies": { "coffee-script": { "version": "1.6.0", - "from": "coffee-script@1.6.0", + "from": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz", "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" } } }, "sixpack-client": { "version": "1.0.0", - "from": "sixpack-client@^1.0.0", + "from": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz", "resolved": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz" }, "temp": { "version": "0.8.3", - "from": "temp@^0.8.3", + "from": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", "dependencies": { "os-tmpdir": { "version": "1.0.1", - "from": "os-tmpdir@^1.0.0", + "from": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz" } } }, "underscore": { "version": "1.6.0", - "from": "underscore@1.6.0", + "from": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "v8-profiler": { "version": "5.6.5", - "from": "v8-profiler@^5.2.3", + "from": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.6.5.tgz", "resolved": "https://registry.npmjs.org/v8-profiler/-/v8-profiler-5.6.5.tgz", "dependencies": { "nan": { - "version": "2.3.5", - "from": "nan@^2.3.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz" + "version": "2.4.0", + "from": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" }, "node-pre-gyp": { - "version": "0.6.28", - "from": "node-pre-gyp@^0.6.5", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.28.tgz", + "version": "0.6.29", + "from": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.29.tgz", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.29.tgz", "dependencies": { "mkdirp": { "version": "0.5.1", - "from": "mkdirp@~0.5.0", + "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "dependencies": { "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8", + "from": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" } } }, "nopt": { "version": "3.0.6", - "from": "nopt@~3.0.1", + "from": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "dependencies": { "abbrev": { - "version": "1.0.7", - "from": "abbrev@1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" + "version": "1.0.9", + "from": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz" } } }, "npmlog": { - "version": "2.0.4", - "from": "npmlog@~2.0.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", + "version": "3.1.2", + "from": "https://registry.npmjs.org/npmlog/-/npmlog-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-3.1.2.tgz", "dependencies": { - "ansi": { - "version": "0.3.1", - "from": "ansi@~0.3.1", - "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz" - }, "are-we-there-yet": { "version": "1.1.2", - "from": "are-we-there-yet@~1.1.2", + "from": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz", "dependencies": { "delegates": { "version": "1.0.0", - "from": "delegates@^1.0.0", + "from": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" }, "readable-stream": { "version": "2.1.4", - "from": "readable-stream@^2.0.0 || ^1.1.13", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "dependencies": { "buffer-shims": { "version": "1.0.0", - "from": "buffer-shims@^1.0.0", + "from": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" }, "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "isarray": { "version": "1.0.0", - "from": "isarray@~1.0.0", + "from": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "process-nextick-args": { "version": "1.0.7", - "from": "process-nextick-args@~1.0.6", + "from": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@~1.0.1", + "from": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } } } }, + "console-control-strings": { + "version": "1.1.0", + "from": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + }, "gauge": { - "version": "1.2.7", - "from": "gauge@~1.2.5", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", + "version": "2.6.0", + "from": "https://registry.npmjs.org/gauge/-/gauge-2.6.0.tgz", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.6.0.tgz", "dependencies": { + "aproba": { + "version": "1.0.4", + "from": "https://registry.npmjs.org/aproba/-/aproba-1.0.4.tgz", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.0.4.tgz" + }, + "has-color": { + "version": "0.1.7", + "from": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz" + }, "has-unicode": { - "version": "2.0.0", - "from": "has-unicode@^2.0.0", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.0.tgz" + "version": "2.0.1", + "from": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" }, - "lodash.pad": { - "version": "4.4.0", - "from": "lodash.pad@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.4.0.tgz", + "object-assign": { + "version": "4.1.0", + "from": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz" + }, + "signal-exit": { + "version": "3.0.0", + "from": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.0.tgz" + }, + "string-width": { + "version": "1.0.1", + "from": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz", "dependencies": { - "lodash._baseslice": { - "version": "4.0.0", - "from": "lodash._baseslice@~4.0.0", - "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz" + "code-point-at": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz", + "dependencies": { + "number-is-nan": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz" + } + } }, - "lodash._basetostring": { - "version": "4.12.0", - "from": "lodash._basetostring@~4.12.0", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz" - }, - "lodash.tostring": { - "version": "4.1.3", - "from": "lodash.tostring@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz" + "is-fullwidth-code-point": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "dependencies": { + "number-is-nan": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz" + } + } } } }, - "lodash.padend": { - "version": "4.5.0", - "from": "lodash.padend@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.5.0.tgz", + "strip-ansi": { + "version": "3.0.1", + "from": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "dependencies": { - "lodash._baseslice": { - "version": "4.0.0", - "from": "lodash._baseslice@~4.0.0", - "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz" - }, - "lodash._basetostring": { - "version": "4.12.0", - "from": "lodash._basetostring@~4.12.0", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz" - }, - "lodash.tostring": { - "version": "4.1.3", - "from": "lodash.tostring@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz" + "ansi-regex": { + "version": "2.0.0", + "from": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" } } }, - "lodash.padstart": { - "version": "4.5.0", - "from": "lodash.padstart@^4.1.0", - "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.5.0.tgz", - "dependencies": { - "lodash._baseslice": { - "version": "4.0.0", - "from": "lodash._baseslice@~4.0.0", - "resolved": "https://registry.npmjs.org/lodash._baseslice/-/lodash._baseslice-4.0.0.tgz" - }, - "lodash._basetostring": { - "version": "4.12.0", - "from": "lodash._basetostring@~4.12.0", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz" - }, - "lodash.tostring": { - "version": "4.1.3", - "from": "lodash.tostring@^4.0.0", - "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.3.tgz" - } - } + "wide-align": { + "version": "1.1.0", + "from": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.0.tgz" } } + }, + "set-blocking": { + "version": "2.0.0", + "from": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" } } }, "rc": { "version": "1.1.6", - "from": "rc@~1.1.0", + "from": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", "dependencies": { "deep-extend": { "version": "0.4.1", - "from": "deep-extend@~0.4.0", + "from": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" }, "ini": { "version": "1.3.4", - "from": "ini@~1.3.0", + "from": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" }, "minimist": { "version": "1.2.0", - "from": "minimist@^1.2.0", + "from": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" }, "strip-json-comments": { "version": "1.0.4", - "from": "strip-json-comments@~1.0.4", + "from": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" } } }, "rimraf": { - "version": "2.5.2", - "from": "rimraf@~2.5.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", + "version": "2.5.4", + "from": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", "dependencies": { "glob": { - "version": "7.0.3", - "from": "glob@^7.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "version": "7.0.5", + "from": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", "dependencies": { + "fs.realpath": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, "inflight": { "version": "1.0.5", - "from": "inflight@^1.0.4", + "from": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { - "version": "3.0.0", - "from": "minimatch@2 || 3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -3654,19 +3927,19 @@ }, "once": { "version": "1.3.3", - "from": "once@^1.3.0", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "path-is-absolute": { "version": "1.0.0", - "from": "path-is-absolute@^1.0.0", + "from": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" } } @@ -3674,101 +3947,101 @@ } }, "semver": { - "version": "5.1.0", - "from": "semver@~5.1.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" + "version": "5.2.0", + "from": "https://registry.npmjs.org/semver/-/semver-5.2.0.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.2.0.tgz" }, "tar": { "version": "2.2.1", - "from": "tar@~2.2.0", + "from": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", "dependencies": { "block-stream": { "version": "0.0.9", - "from": "block-stream@*", + "from": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" }, "fstream": { - "version": "1.0.9", - "from": "fstream@^1.0.2", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz", + "version": "1.0.10", + "from": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", "dependencies": { "graceful-fs": { - "version": "4.1.4", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz" + "version": "4.1.5", + "from": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz" } } }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "tar-pack": { - "version": "3.1.3", - "from": "tar-pack@~3.1.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.3.tgz", + "version": "3.1.4", + "from": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.4.tgz", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.1.4.tgz", "dependencies": { "debug": { "version": "2.2.0", - "from": "debug@~2.2.0", + "from": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", "dependencies": { "ms": { "version": "0.7.1", - "from": "ms@0.7.1", + "from": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" } } }, "fstream": { - "version": "1.0.9", - "from": "fstream@~1.0.8", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz", + "version": "1.0.10", + "from": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.10.tgz", "dependencies": { "graceful-fs": { - "version": "4.1.4", - "from": "graceful-fs@^4.1.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz" + "version": "4.1.5", + "from": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.5.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.0", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, "fstream-ignore": { "version": "1.0.5", - "from": "fstream-ignore@~1.0.3", + "from": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", "dependencies": { "inherits": { "version": "2.0.1", - "from": "inherits@2", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "minimatch": { - "version": "3.0.0", - "from": "minimatch@^3.0.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "version": "3.0.3", + "from": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.4", - "from": "brace-expansion@^1.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", + "version": "1.1.6", + "from": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", "dependencies": { "balanced-match": { - "version": "0.4.1", - "from": "balanced-match@^0.4.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + "version": "0.4.2", + "from": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" }, "concat-map": { "version": "0.0.1", - "from": "concat-map@0.0.1", + "from": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" } } @@ -3779,56 +4052,61 @@ }, "once": { "version": "1.3.3", - "from": "once@~1.3.3", + "from": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "dependencies": { "wrappy": { "version": "1.0.2", - "from": "wrappy@1", + "from": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" } } }, "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@~2.0.4", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "version": "2.1.4", + "from": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.4.tgz", "dependencies": { + "buffer-shims": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" + }, "core-util-is": { "version": "1.0.2", - "from": "core-util-is@~1.0.0", + "from": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1", + "from": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, "isarray": { "version": "1.0.0", - "from": "isarray@~1.0.0", + "from": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" }, "process-nextick-args": { "version": "1.0.7", - "from": "process-nextick-args@~1.0.6", + "from": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, "string_decoder": { "version": "0.10.31", - "from": "string_decoder@~0.10.x", + "from": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, "util-deprecate": { "version": "1.0.2", - "from": "util-deprecate@~1.0.1", + "from": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } }, "uid-number": { "version": "0.0.6", - "from": "uid-number@~0.0.6", + "from": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" } } @@ -3839,12 +4117,12 @@ }, "xml2js": { "version": "0.2.0", - "from": "xml2js@0.2.0", + "from": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz", "dependencies": { "sax": { "version": "1.2.1", - "from": "sax@>=0.1.1", + "from": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" } } diff --git a/services/web/package.json b/services/web/package.json index 0c0c4bb436..8700d92558 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -46,6 +46,10 @@ "nodemailer-sendgrid-transport": "^0.2.0", "nodemailer-ses-transport": "^1.3.0", "optimist": "0.6.1", + "passport": "^0.3.2", + "passport-local": "^1.0.0", + "pg": "^6.0.3", + "pg-hstore": "^2.3.2", "redback": "0.4.0", "redis": "0.10.1", "redis-sharelatex": "0.0.9", @@ -53,6 +57,7 @@ "requests": "^0.1.7", "rimraf": "2.2.6", "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", diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index ec2e1dc0bb..8bcf610640 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -112,6 +112,8 @@ define [ [asyncFormCtrl, ngModelCtrl] = ctrl ngModelCtrl.$parsers.unshift (modelValue) -> + + isValid = passField.validatePass() email = asyncFormCtrl.getEmail() || window.usersEmail if !isValid @@ -121,5 +123,8 @@ define [ if modelValue.indexOf(email) != -1 or modelValue.indexOf(startOfEmail) != -1 isValid = false scope.complexPasswordErrorMessage = "Password can not contain email address" + if opts.length.max? and modelValue.length == opts.length.max + isValid = false + scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached" ngModelCtrl.$setValidity('complexPassword', isValid) return modelValue diff --git a/services/web/public/coffee/directives/creditCards.coffee b/services/web/public/coffee/directives/creditCards.coffee new file mode 100644 index 0000000000..3ee89a610a --- /dev/null +++ b/services/web/public/coffee/directives/creditCards.coffee @@ -0,0 +1,539 @@ +define [ + "base" +], (App) -> + App.factory 'ccUtils', () -> + defaultFormat = /(\d{1,4})/g; + defaultInputFormat = /(?:^|\s)(\d{4})$/ + + cards = [ + # Credit cards + { + type: 'visa' + patterns: [4] + format: defaultFormat + length: [13, 16] + cvcLength: [3] + luhn: true + } + { + type: 'mastercard' + patterns: [ + 51, 52, 53, 54, 55, + 22, 23, 24, 25, 26, 27 + ] + format: defaultFormat + length: [16] + cvcLength: [3] + luhn: true + } + { + type: 'amex' + patterns: [34, 37] + format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/ + length: [15] + cvcLength: [3..4] + luhn: true + } + { + type: 'dinersclub' + patterns: [30, 36, 38, 39] + format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/ + length: [14] + cvcLength: [3] + luhn: true + } + { + type: 'discover' + patterns: [60, 64, 65, 622] + format: defaultFormat + length: [16] + cvcLength: [3] + luhn: true + } + { + type: 'unionpay' + patterns: [62, 88] + format: defaultFormat + length: [16..19] + cvcLength: [3] + luhn: false + } + { + type: 'jcb' + patterns: [35] + format: defaultFormat + length: [16] + cvcLength: [3] + luhn: true + } + ] + + cardFromNumber = (num) -> + num = (num + '').replace(/\D/g, "") + for card in cards + for pattern in card.patterns + p = pattern + "" + return card if num.substr(0, p.length) == p + + cardFromType = (type) -> + return card for card in cards when card.type is type + + cardType = (num) -> + return null unless num + cardFromNumber(num)?.type or null + + formatCardNumber = (num) -> + num = num.replace(/\D/g, '') + card = cardFromNumber(num) + return num unless card + + upperLength = card.length[card.length.length - 1] + num = num[0...upperLength] + + if card.format.global + num.match(card.format)?.join(' ') + else + groups = card.format.exec(num) + return unless groups? + groups.shift() + groups = $.grep(groups, (n) -> n) # Filter empty groups + groups.join(' ') + + formatExpiry = (expiry) -> + parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/) + return '' unless parts + + mon = parts[1] || '' + sep = parts[2] || '' + year = parts[3] || '' + + if year.length > 0 + sep = ' / ' + + else if sep is ' /' + mon = mon.substring(0, 1) + sep = '' + + else if mon.length == 2 or sep.length > 0 + sep = ' / ' + + else if mon.length == 1 and mon not in ['0', '1'] + mon = "0#{mon}" + sep = ' / ' + + return mon + sep + year + + parseExpiry = (value = "") -> + [month, year] = value.split(/[\s\/]+/, 2) + + # Allow for year shortcut + if year?.length is 2 and /^\d+$/.test(year) + prefix = (new Date).getFullYear() + prefix = prefix.toString()[0..1] + year = prefix + year + + month = parseInt(month, 10) + year = parseInt(year, 10) + + return unless !isNaN(month) and !isNaN(year) + + month: month, year: year + + return { + fromNumber: cardFromNumber + fromType: cardFromType + cardType: cardType + formatExpiry: formatExpiry + formatCardNumber: formatCardNumber + defaultFormat: defaultFormat + defaultInputFormat: defaultInputFormat + parseExpiry: parseExpiry + } + + App.factory 'ccFormat', (ccUtils, $filter) -> + hasTextSelected = ($target) -> + # If some text is selected + return true if $target.prop('selectionStart')? and + $target.prop('selectionStart') isnt $target.prop('selectionEnd') + + # If some text is selected in IE + if document?.selection?.createRange? + return true if document.selection.createRange().text + + false + + safeVal = (value, $target) -> + try + cursor = $target.prop('selectionStart') + catch error + cursor = null + + last = $target.val() + $target.val(value) + + if cursor != null && $target.is(":focus") + cursor = value.length if cursor is last.length + + # This hack looks for scenarios where we are changing an input's value such + # that "X| " is replaced with " |X" (where "|" is the cursor). In those + # scenarios, we want " X|". + # + # For example: + # 1. Input field has value "4444| " + # 2. User types "1" + # 3. Input field has value "44441| " + # 4. Reformatter changes it to "4444 |1" + # 5. By incrementing the cursor, we make it "4444 1|" + # + # This is awful, and ideally doesn't go here, but given the current design + # of the system there does not appear to be a better solution. + # + # Note that we can't just detect when the cursor-1 is " ", because that + # would incorrectly increment the cursor when backspacing, e.g. pressing + # backspace in this scenario: "4444 1|234 5". + if last != value + prevPair = last[cursor-1..cursor] + currPair = value[cursor-1..cursor] + digit = value[cursor] + cursor = cursor + 1 if /\d/.test(digit) and + prevPair == "#{digit} " and currPair == " #{digit}" + + $target.prop('selectionStart', cursor) + $target.prop('selectionEnd', cursor) + + # Replace Full-Width Chars + replaceFullWidthChars = (str = '') -> + fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19' + halfWidth = '0123456789' + + value = '' + chars = str.split('') + + # Avoid using reserved word `char` + for chr in chars + idx = fullWidth.indexOf(chr) + chr = halfWidth[idx] if idx > -1 + value += chr + + value + + # Format Numeric + reFormatNumeric = (e) -> + $target = $(e.currentTarget) + setTimeout -> + value = $target.val() + value = replaceFullWidthChars(value) + value = value.replace(/\D/g, '') + safeVal(value, $target) + + # Format Card Number + reFormatCardNumber = (e) -> + $target = $(e.currentTarget) + setTimeout -> + value = $target.val() + value = replaceFullWidthChars(value) + value = ccUtils.formatCardNumber(value) + safeVal(value, $target) + + formatCardNumber = (e) -> + # Only format if input is a number + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + $target = $(e.currentTarget) + value = $target.val() + card = ccUtils.fromNumber(value + digit) + length = (value.replace(/\D/g, '') + digit).length + + upperLength = 16 + upperLength = card.length[card.length.length - 1] if card + return if length >= upperLength + + # Return if focus isn't at the end of the text + return if $target.prop('selectionStart')? and + $target.prop('selectionStart') isnt value.length + + if card && card.type is 'amex' + # AMEX cards are formatted differently + re = /^(\d{4}|\d{4}\s\d{6})$/ + else + re = /(?:^|\s)(\d{4})$/ + + # If '4242' + 4 + if re.test(value) + e.preventDefault() + setTimeout -> $target.val(value + ' ' + digit) + + # If '424' + 2 + else if re.test(value + digit) + e.preventDefault() + setTimeout -> $target.val(value + digit + ' ') + + formatBackCardNumber = (e) -> + $target = $(e.currentTarget) + value = $target.val() + + # Return unless backspacing + return unless e.which is 8 + + # Return if focus isn't at the end of the text + return if $target.prop('selectionStart')? and + $target.prop('selectionStart') isnt value.length + + # Remove the digit + trailing space + if /\d\s$/.test(value) + e.preventDefault() + setTimeout -> $target.val(value.replace(/\d\s$/, '')) + # Remove digit if ends in space + digit + else if /\s\d?$/.test(value) + e.preventDefault() + setTimeout -> $target.val(value.replace(/\d$/, '')) + + getFormattedCardNumber = (num) -> + num = num.replace(/\D/g, '') + card = ccUtils.fromNumber(num) + return num unless card + + upperLength = card.length[card.length.length - 1] + num = num[0...upperLength] + + if card.format.global + num.match(card.format)?.join(' ') + else + groups = card.format.exec(num) + return unless groups? + groups.shift() + groups = $.grep(groups, (n) -> n) # Filter empty groups + groups.join(' ') + + parseCardNumber = (value) -> + if value? then value.replace(/\s/g, '') else value + + # Format Expiry + reFormatExpiry = (e) -> + $target = $(e.currentTarget) + setTimeout -> + value = $target.val() + value = replaceFullWidthChars(value) + value = ccUtils.formatExpiry(value) + safeVal(value, $target) + + + formatExpiry = (e) -> + # Only format if input is a number + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + $target = $(e.currentTarget) + val = $target.val() + digit + + if /^\d$/.test(val) and val not in ['0', '1'] + e.preventDefault() + setTimeout -> $target.val("0#{val} / ") + + else if /^\d\d$/.test(val) + e.preventDefault() + setTimeout -> + # Split for months where we have the second digit > 2 (past 12) and turn + # that into (m1)(m2) => 0(m1) / (m2) + m1 = parseInt(val[0], 10) + m2 = parseInt(val[1], 10) + if m2 > 2 and m1 != 0 + $target.val("0#{m1} / #{m2}") + else + $target.val("#{val} / ") + + formatForwardExpiry = (e) -> + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + $target = $(e.currentTarget) + val = $target.val() + + if /^\d\d$/.test(val) + $target.val("#{val} / ") + + formatForwardSlash = (e) -> + which = String.fromCharCode(e.which) + return unless which is '/' or which is ' ' + + $target = $(e.currentTarget) + val = $target.val() + + if /^\d$/.test(val) and val isnt '0' + $target.val("0#{val} / ") + + formatBackExpiry = (e) -> + $target = $(e.currentTarget) + value = $target.val() + + # Return unless backspacing + return unless e.which is 8 + + # Return if focus isn't at the end of the text + return if $target.prop('selectionStart')? and + $target.prop('selectionStart') isnt value.length + + # Remove the trailing space + last digit + if /\d\s\/\s$/.test(value) + e.preventDefault() + setTimeout -> $target.val(value.replace(/\d\s\/\s$/, '')) + + parseExpiry = (value) -> + if value? + dateAsObj = ccUtils.parseExpiry(value) + + return unless dateAsObj? + + expiry = new Date dateAsObj.year, dateAsObj.month - 1 + + return $filter('date')(expiry, 'MM/yyyy') + + # Format CVC + reFormatCVC = (e) -> + $target = $(e.currentTarget) + setTimeout -> + value = $target.val() + value = replaceFullWidthChars(value) + value = value.replace(/\D/g, '')[0...4] + safeVal(value, $target) + + # Restrictions + restrictNumeric = (e) -> + # Key event is for a browser shortcut + return true if e.metaKey or e.ctrlKey + + # If keycode is a space + return false if e.which is 32 + + # If keycode is a special char (WebKit) + return true if e.which is 0 + + # If char is a special char (Firefox) + return true if e.which < 33 + + input = String.fromCharCode(e.which) + + # Char is a number or a space + !!/[\d\s]/.test(input) + + restrictCardNumber = (e) -> + $target = $(e.currentTarget) + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + return if hasTextSelected($target) + + # Restrict number of digits + value = ($target.val() + digit).replace(/\D/g, '') + card = ccUtils.fromNumber(value) + + if card + value.length <= card.length[card.length.length - 1] + else + # All other cards are 16 digits long + value.length <= 16 + + restrictExpiry = (e) -> + $target = $(e.currentTarget) + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + return if hasTextSelected($target) + + value = $target.val() + digit + value = value.replace(/\D/g, '') + + return false if value.length > 6 + + restrictCVC = (e) -> + $target = $(e.currentTarget) + digit = String.fromCharCode(e.which) + return unless /^\d+$/.test(digit) + + return if hasTextSelected($target) + + val = $target.val() + digit + val.length <= 4 + + setCardType = (e) -> + $target = $(e.currentTarget) + val = $target.val() + cardType = ccUtils.cardType(val) or 'unknown' + + unless $target.hasClass(cardType) + allTypes = (card.type for card in cards) + + $target.removeClass('unknown') + $target.removeClass(allTypes.join(' ')) + + $target.addClass(cardType) + $target.toggleClass('identified', cardType isnt 'unknown') + $target.trigger('payment.cardType', cardType) + + return { + hasTextSelected + replaceFullWidthChars + reFormatNumeric + reFormatCardNumber + formatCardNumber + formatBackCardNumber + getFormattedCardNumber + parseCardNumber + reFormatExpiry + formatExpiry + formatForwardExpiry + formatForwardSlash + formatBackExpiry + parseExpiry + reFormatCVC + restrictNumeric + restrictCardNumber + restrictExpiry + restrictCVC + setCardType + } + + App.directive "ccFormatExpiry", (ccFormat) -> + restrict: "A" + require: "ngModel" + link: (scope, el, attrs, ngModel) -> + el.on "keypress", ccFormat.restrictNumeric + el.on "keypress", ccFormat.restrictExpiry + el.on "keypress", ccFormat.formatExpiry + el.on "keypress", ccFormat.formatForwardSlash + el.on "keypress", ccFormat.formatForwardExpiry + el.on "keydown", ccFormat.formatBackExpiry + el.on "change", ccFormat.reFormatExpiry + el.on "input", ccFormat.reFormatExpiry + el.on "paste", ccFormat.reFormatExpiry + + ngModel.$parsers.push ccFormat.parseExpiry + ngModel.$formatters.push ccFormat.parseExpiry + + App.directive "ccFormatCardNumber", (ccFormat) -> + restrict: "A" + require: "ngModel" + link: (scope, el, attrs, ngModel) -> + el.on "keypress", ccFormat.restrictNumeric + el.on "keypress", ccFormat.restrictCardNumber + el.on "keypress", ccFormat.formatCardNumber + el.on "keydown", ccFormat.formatBackCardNumber + el.on "paste", ccFormat.reFormatCardNumber + + ngModel.$parsers.push ccFormat.parseCardNumber + ngModel.$formatters.push ccFormat.getFormattedCardNumber + + App.directive "ccFormatSecCode", (ccFormat) -> + restrict: "A" + require: "ngModel" + link: (scope, el, attrs, ngModel) -> + el.on "keypress", ccFormat.restrictNumeric + el.on "keypress", ccFormat.restrictCVC + el.on "paste", ccFormat.reFormatCVC + el.on "change", ccFormat.reFormatCVC + el.on "input", ccFormat.reFormatCVC + + + + \ No newline at end of file diff --git a/services/web/public/coffee/filters/formatDate.coffee b/services/web/public/coffee/filters/formatDate.coffee index 395194456e..5b8c9e10c6 100644 --- a/services/web/public/coffee/filters/formatDate.coffee +++ b/services/web/public/coffee/filters/formatDate.coffee @@ -1,6 +1,6 @@ define [ "base" - "libs/moment-2.9.0" + "moment" ], (App, moment) -> moment.locale "en", calendar: lastDay : '[Yesterday]' @@ -16,4 +16,4 @@ define [ App.filter "relativeDate", () -> (date) -> - moment(date).calendar() \ No newline at end of file + moment(date).calendar() diff --git a/services/web/public/coffee/filters/wrapLongWords.coffee b/services/web/public/coffee/filters/wrapLongWords.coffee new file mode 100644 index 0000000000..db502e848a --- /dev/null +++ b/services/web/public/coffee/filters/wrapLongWords.coffee @@ -0,0 +1,25 @@ +define [ + "base" +], (App) -> + DEF_MIN_LENGTH = 20 + + _decodeHTMLEntities = (str) -> + str.replace /(\d+);/g, (match, dec) -> + String.fromCharCode dec; + + _getWrappedWordsString = (baseStr, wrapperElName, minLength) -> + minLength = minLength || DEF_MIN_LENGTH + words = baseStr.split ' ' + + wordsWrapped = for word in words + if _decodeHTMLEntities(word).length >= minLength + "<#{wrapperElName} class=\"break-word\">#{word}#{wrapperElName}>" + else + word + + outputStr = wordsWrapped.join ' ' + + + App.filter "wrapLongWords", () -> + (input, minLength) -> + _getWrappedWordsString input, "span", minLength \ No newline at end of file diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 4c75ab5d77..607b8556aa 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -4,11 +4,12 @@ define [ "ide/connection/ConnectionManager" "ide/editor/EditorManager" "ide/online-users/OnlineUsersManager" - "ide/track-changes/TrackChangesManager" + "ide/history/HistoryManager" "ide/permissions/PermissionsManager" "ide/pdf/PdfManager" "ide/binary-files/BinaryFilesManager" "ide/references/ReferencesManager" + "ide/SafariScrollPatcher" "ide/settings/index" "ide/share/index" "ide/chat/index" @@ -35,14 +36,15 @@ define [ ConnectionManager EditorManager OnlineUsersManager - TrackChangesManager + HistoryManager PermissionsManager PdfManager BinaryFilesManager ReferencesManager + SafariScrollPatcher ) -> - App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) -> + App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking) -> # Don't freak out if we're already in an apply callback $scope.$originalApply = $scope.$apply $scope.$apply = (fn = () ->) -> @@ -69,19 +71,42 @@ define [ $scope.chat = {} + + # Only run the header AB test for newly registered users. + _abTestStartDate = new Date(Date.UTC(2016, 8, 28)) + _userSignUpDate = new Date(window.user.signUpDate) + + $scope.shouldABTestHeaderLabels = _userSignUpDate > _abTestStartDate + $scope.headerLabelsABVariant = "" + + if ($scope.shouldABTestHeaderLabels) + sixpack.participate "editor-header", [ "default", "labels"], (chosenVariation) -> + $scope.headerLabelsABVariant = chosenVariation + + $scope.trackABTestConversion = (headerItem) -> + event_tracking.sendMB "header-ab-conversion", { + headerItem: headerItem, + variant: $scope.headerLabelsABVariant + } + # Tracking code. $scope.$watch "ui.view", (newView, oldView) -> if newView? and newView != "editor" and newView != "pdf" - event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once" + event_tracking.sendMBOnce "ide-open-view-#{ newView }-once" $scope.$watch "ui.chatOpen", (isOpen) -> - event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen + event_tracking.sendMBOnce "ide-open-chat-once" if isOpen $scope.$watch "ui.leftMenuShown", (isOpen) -> - event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen + event_tracking.sendMBOnce "ide-open-left-menu-once" if isOpen + + $scope.trackHover = (feature) -> + event_tracking.sendMBOnce "ide-hover-#{feature}-once" # End of tracking code. window._ide = ide + + ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and / ide.project_id = $scope.project_id = window.project_id ide.$scope = $scope @@ -91,7 +116,7 @@ define [ ide.fileTreeManager = new FileTreeManager(ide, $scope) ide.editorManager = new EditorManager(ide, $scope) ide.onlineUsersManager = new OnlineUsersManager(ide, $scope) - ide.trackChangesManager = new TrackChangesManager(ide, $scope) + ide.historyManager = new HistoryManager(ide, $scope) ide.pdfManager = new PdfManager(ide, $scope) ide.permissionsManager = new PermissionsManager(ide, $scope) ide.binaryFilesManager = new BinaryFilesManager(ide, $scope) @@ -135,6 +160,9 @@ define [ catch err console.error err + if ide.browserIsSafari + ide.safariScrollPatcher = new SafariScrollPatcher($scope) + # User can append ?ft=somefeature to url to activate a feature toggle ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1] diff --git a/services/web/public/coffee/ide/SafariScrollPatcher.coffee b/services/web/public/coffee/ide/SafariScrollPatcher.coffee new file mode 100644 index 0000000000..07ad2e7565 --- /dev/null +++ b/services/web/public/coffee/ide/SafariScrollPatcher.coffee @@ -0,0 +1,78 @@ +define [ +], () -> + class SafariScrollPatcher + constructor: ($scope) -> + @isOverAce = false # Flag to control if the pointer is over Ace. + @pdfDiv = null + @aceDiv = null + + # Start listening to PDF wheel events when the pointer leaves the PDF region. + # P.S. This is the problem in a nutshell: although the pointer is elsewhere, + # wheel events keep being dispatched to the PDF. + @handlePdfDivMouseLeave = () => + @pdfDiv.addEventListener "wheel", @dispatchToAce + + # Stop listening to wheel events when the pointer enters the PDF region. If + # the pointer is over the PDF, native behaviour is adequate. + @handlePdfDivMouseEnter = () => + @pdfDiv.removeEventListener "wheel", @dispatchToAce + + # Set the "pointer over Ace" flag as false, when the mouse leaves its area. + @handleAceDivMouseLeave = () => + @isOverAce = false + + # Set the "pointer over Ace" flag as true, when the mouse enters its area. + @handleAceDivMouseEnter = () => + @isOverAce = true + + # Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners. + # If elements are already defined, clear existing event listeners and do + # the process again (grab elements, set listeners). + @setListeners = () => + @isOverAce = false + + # If elements aren't null, remove existing listeners. + if @pdfDiv? + @pdfDiv.removeEventListener @handlePdfDivMouseLeave + @pdfDiv.removeEventListener @handlePdfDivMouseEnter + + if @aceDiv? + @aceDiv.removeEventListener @handleAceDivMouseLeave + @aceDiv.removeEventListener @handleAceDivMouseEnter + + # Grab elements. + @pdfDiv = document.querySelector ".pdfjs-viewer" # Grab the PDF div. + @aceDiv = document.querySelector ".ace_content" # Also the editor. + + # Set hover-related listeners. + @pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave + @pdfDiv.addEventListener "mouseenter", @handlePdfDivMouseEnter + @aceDiv.addEventListener "mouseleave", @handleAceDivMouseLeave + @aceDiv.addEventListener "mouseenter", @handleAceDivMouseEnter + + # Handler for wheel events on the PDF. + # If the pointer is over Ace, grab the event, prevent default behaviour + # and dispatch it to Ace. + @dispatchToAce = (e) => + if @isOverAce + # If this is logged, the problem just happened: the event arrived + # here (the PDF wheel handler), but it should've gone to Ace. + + # Small timeout - if we dispatch immediately, an exception is thrown. + window.setTimeout(() => + # Dispatch the exact same event to Ace (this will keep values + # values e.g. `wheelDelta` consistent with user interaction). + @aceDiv.dispatchEvent e + , 1) + + # Avoid scrolling the PDF, as we assume this was intended to the + # editor. + e.preventDefault() + + # "loaded" event is emitted from the pdfViewer controller $scope. This means + # that the previous PDF DOM element was destroyed and a new one is available, + # so we need to grab the elements and set the listeners again. + $scope.$on "loaded", () => + @setListeners() + + diff --git a/services/web/public/coffee/ide/chat/index.coffee b/services/web/public/coffee/ide/chat/index.coffee index d5a537903b..de9c46d62d 100644 --- a/services/web/public/coffee/ide/chat/index.coffee +++ b/services/web/public/coffee/ide/chat/index.coffee @@ -3,4 +3,5 @@ define [ "ide/chat/controllers/ChatController" "ide/chat/controllers/ChatMessageController" "ide/chat/directives/mathjax" + "filters/wrapLongWords" ], () -> \ No newline at end of file diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 54d56cac19..9c6a124f1a 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -46,8 +46,13 @@ define [ done = () => if options.gotoLine? - @$scope.$broadcast "editor:gotoLine", options.gotoLine - + # allow Ace to display document before moving, delay until next tick + # added delay to make this happen later that gotoStoredPosition in + # CursorPositionManager + setTimeout () => + @$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn + ,0 + if doc.id == @$scope.editor.open_doc_id and !options.forceReopen @$scope.$apply () => done() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 800d6bd194..2dc9440ffa 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -7,9 +7,16 @@ define [ "ide/editor/directives/aceEditor/spell-check/SpellCheckManager" "ide/editor/directives/aceEditor/highlights/HighlightsManager" "ide/editor/directives/aceEditor/cursor-position/CursorPositionManager" -], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager) -> + "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" +], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) -> EditSession = ace.require('ace/edit_session').EditSession - + + # set the path for ace workers if using a CDN (from editor.jade) + if window.aceWorkerPath != "" + ace.config.set('workerPath', "#{window.aceWorkerPath}") + else + ace.config.setDefaultValue("session", "useWorker", false) + # Ace loads its script itself, so we need to hook in to be able to clear # the cache. if !ace.config._moduleUrl? @@ -37,6 +44,7 @@ define [ annotations: "=" navigateHighlights: "=", onCtrlEnter: "=" + syntaxValidation: "=" } link: (scope, element, attrs) -> # Don't freak out if we're already in an apply callback @@ -62,6 +70,9 @@ define [ undoManager = new UndoManager(scope, editor, element) highlightsManager = new HighlightsManager(scope, editor, element) cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) + trackChangesManager = new TrackChangesManager(scope, editor, element) + if window.location.search.match /tcon=true/ # track changes on + trackChangesManager.enabled = true # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand @@ -174,7 +185,6 @@ define [ editor.setValue(text, -1) session = editor.getSession() session.setUseWrapMode(true) - session.setMode("ace/mode/latex") scope.$watch "annotations", (annotations) -> session = editor.getSession() @@ -183,6 +193,10 @@ define [ scope.$watch "readOnly", (value) -> editor.setReadOnly !!value + scope.$watch "syntaxValidation", (value) -> + session = editor.getSession() + session.setOption("useWorker", value); + editor.setOption("scrollPastEnd", true) resetSession = () -> @@ -199,7 +213,10 @@ define [ attachToAce = (sharejs_doc) -> lines = sharejs_doc.getSnapshot().split("\n") - editor.setSession(new EditSession(lines)) + session = editor.getSession() + if session? + session.destroy() + editor.setSession(new EditSession(lines, "ace/mode/latex")) resetSession() session = editor.getSession() @@ -209,7 +226,9 @@ define [ sharejs_doc.on "remoteop.recordForUndo", () => undoManager.nextUpdateIsRemote = true + editor.initing = true sharejs_doc.attachToAce(editor) + editor.initing = false # need to set annotations after attaching because attaching # deletes and then inserts document content session.setAnnotations scope.annotations diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee index 7019b213b5..fd1b2d7830 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee @@ -23,10 +23,10 @@ define [], () -> @storeCursorPosition(@editor.getSession()) @storeScrollTopPosition(@editor.getSession()) - @$scope.$on "#{@$scope.name}:gotoLine", (editor, value) => - if value? + @$scope.$on "#{@$scope.name}:gotoLine", (editor, line, column) => + if line? setTimeout () => - @gotoLine(value) + @gotoLine(line, column) , 10 # Hack: Must happen after @gotoStoredPosition storeScrollTopPosition: (session) -> @@ -53,6 +53,7 @@ define [], () -> @editor.getSession().setScrollTop(pos.scrollTop or 0) delete @ignoreCursorPositionChanges - gotoLine: (line) -> - @editor.gotoLine(line) - @editor.focus() \ No newline at end of file + gotoLine: (line, column) -> + @editor.gotoLine(line, column) + @editor.scrollToLine(line,true,true) # centre and animate + @editor.focus() diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee index 95a6519d59..e84ce1d785 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee @@ -213,7 +213,9 @@ define [ positions = [] for line, row in lines if !linesToProcess? or linesToProcess[row] - wordRegex = /\\?['a-zA-Z\u00C0-\u017F]+/g + # Regex generated from /\\?['\p{L}]+/g via https://mothereff.in/regexpu. + # \p{L} matches unicode characters in the 'letter' category, but is not supported until ES6. + wordRegex = /\\?(?:['A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FD5\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF30-\uDF40\uDF42-\uDF49\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F]|\uD808[\uDC00-\uDF99]|\uD809[\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00\uDC01]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1]|\uD87E[\uDC00-\uDE1D])+/g while (result = wordRegex.exec(line)) word = result[0] if word[0] == "'" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee new file mode 100644 index 0000000000..1c092ad190 --- /dev/null +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -0,0 +1,410 @@ +define [ + "ace/ace" + "utils/EventEmitter" +], (_, EventEmitter) -> + class TrackChangesManager + Range = ace.require("ace/range").Range + + constructor: (@$scope, @editor, @element) -> + @changesTracker = new ChangesTracker() + @changeIdToMarkerIdMap = {} + @enabled = false + + @changesTracker.on "insert:added", (change) => + @_onInsertAdded(change) + @changesTracker.on "insert:removed", (change) => + @_onInsertRemoved(change) + @changesTracker.on "delete:added", (change) => + @_onDeleteAdded(change) + @changesTracker.on "delete:removed", (change) => + @_onDeleteRemoved(change) + @changesTracker.on "changes:moved", (changes) => + @_onChangesMoved(changes) + + onChange = (e) => + if !@editor.initing and @enabled + @applyChange(e) + setTimeout () => + @checkMapping() + , 100 + + @editor.on "changeSession", (e) => + e.oldSession?.getDocument().off "change", onChange + e.session.getDocument().on "change", onChange + @editor.getSession().getDocument().on "change", onChange + + checkMapping: () -> + session = @editor.getSession() + + # Make a copy of session.getMarkers() so we can modify it + markers = {} + for marker_id, marker of session.getMarkers() + markers[marker_id] = marker + + for change in @changesTracker.changes + op = change.op + marker_id = @changeIdToMarkerIdMap[change.id] + + start = @_shareJsOffsetToAcePosition(op.p) + if op.i? + end = @_shareJsOffsetToAcePosition(op.p + op.i.length) + else if op.d? + end = start + + marker = markers[marker_id] + delete markers[marker_id] + if marker.range.start.row != start.row or + marker.range.start.column != start.column or + marker.range.end.row != end.row or + marker.range.end.column != end.column + console.error "Change doesn't match marker anymore", {change, marker, start, end} + + for marker_id, marker of markers + if marker.clazz.match("track-changes") + console.error "Orphaned ace marker", marker + + applyChange: (delta) -> + op = @_aceChangeToShareJs(delta) + console.log "Applying change", delta, op + @changesTracker.applyOp(op) + + _onInsertAdded: (change) -> + start = @_shareJsOffsetToAcePosition(change.op.p) + end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) + session = @editor.getSession() + doc = session.getDocument() + ace_range = new Range(start.row, start.column, end.row, end.column) + marker_id = session.addMarker(ace_range, "track-changes-added-marker", "text") + @changeIdToMarkerIdMap[change.id] = marker_id + + _onDeleteAdded: (change) -> + position = @_shareJsOffsetToAcePosition(change.op.p) + session = @editor.getSession() + doc = session.getDocument() + ace_range = new Range(position.row, position.column, position.row, position.column) + + # Our delete marker is zero characters wide, but Ace doesn't draw ranges + # that are empty. So we monkey patch the range to tell Ace it's not empty. + # This is the code we need to trick: + # var range = marker.range.clipRows(config.firstRow, config.lastRow); + # if (range.isEmpty()) continue; + _clipRows = ace_range.clipRows + ace_range.clipRows = (args...) -> + range = _clipRows.apply(ace_range, args) + range.isEmpty = () -> + false + return range + + marker_id = session.addMarker(ace_range, "track-changes-deleted-marker", "text") + @changeIdToMarkerIdMap[change.id] = marker_id + + _onInsertRemoved: (change) -> + marker_id = @changeIdToMarkerIdMap[change.id] + session = @editor.getSession() + session.removeMarker marker_id + + _onDeleteRemoved: (change) -> + marker_id = @changeIdToMarkerIdMap[change.id] + session = @editor.getSession() + session.removeMarker marker_id + + _aceChangeToShareJs: (delta) -> + start = delta.start + lines = @editor.getSession().getDocument().getLines 0, start.row + offset = 0 + for line, i in lines + offset += if i < start.row + line.length + else + start.column + offset += start.row # Include newlines + + text = delta.lines.join('\n') + switch delta.action + when 'insert' + return { i: text, p: offset } + when 'remove' + return { d: text, p: offset } + else throw new Error "unknown action: #{delta.action}" + + _shareJsOffsetToAcePosition: (offset) -> + lines = @editor.getSession().getDocument().getAllLines() + row = 0 + for line, row in lines + break if offset <= line.length + offset -= lines[row].length + 1 # + 1 for newline char + return {row:row, column:offset} + + _onChangesMoved: (changes) -> + session = @editor.getSession() + markers = session.getMarkers() + for change in changes + start = @_shareJsOffsetToAcePosition(change.op.p) + if change.op.i? + end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) + else + end = start + marker_id = @changeIdToMarkerIdMap[change.id] + marker = markers[marker_id] + console.log "moving marker", {marker, start, end, change} + marker.range.start = start + marker.range.end = end + + class ChangesTracker extends EventEmitter + # The purpose of this class is to track a set of inserts and deletes to a document, like + # track changes in Word. We store these as a set of ShareJs style ranges: + # {i: "foo", p: 42} # Insert 'foo' at offset 42 + # {d: "bar", p: 37} # Delete 'bar' at offset 37 + # We only track the inserts and deletes, not the whole document, but by being given all + # updates that are applied to a document, we can update these appropriately. + # + # Note that the set of inserts and deletes we store applies to the document as-is at the moment. + # So inserts correspond to text which is in the document, while deletes correspond to text which + # is no longer there, so their lengths do not affect the position of later offsets. + # E.g. + # this is the current text of the document + # |-----| | + # {i: "current ", p:12} -^ ^- {d: "old ", p: 31} + # + # Track changes rules (should be consistent with Word): + # * When text is inserted at a delete, the text goes to the left of the delete + # I.e. "foo|bar" -> "foobaz|bar", where | is the delete, and 'baz' is inserted + # * Deleting content flagged as 'inserted' does not create a new delete marker, it only + # removes the insert marker. E.g. + # * "abdefghijkl" -> "abfghijkl" when 'de' is deleted. No delete marker added + # |---| <- inserted |-| <- inserted + # * Deletes overlapping regular text and inserted text will insert a delete marker for the + # regular text: + # "abcdefghijkl" -> "abcdejkl" when 'fghi' is deleted + # |----| |--|| + # ^- inserted 'bcdefg' \ ^- deleted 'hi' + # \--inserted 'bcde' + # * Deletes overlapping other deletes are merged. E.g. + # "abcghijkl" -> "ahijkl" when 'bcg is deleted' + # | <- delete 'def' | <- delete 'bcdefg' + constructor: () -> + # Change objects have the following structure: + # { + # id: ... # Uniquely generated by us + # op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d) + # i: "..." + # p: 42 + # } + # } + # + # Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in + # sync with Ace ranges. + @changes = [] + @id = 0 + + applyOp: (op) -> + # Apply an op that has been applied to the document to our changes to keep them up to date + if op.i? + @applyInsert(op) + else if op.d? + @applyDelete(op) + + applyInsert: (op) -> + op_start = op.p + op_length = op.i.length + op_end = op.p + op_length + + already_merged = false + previous_change = null + moved_changes = [] + for change in @changes + change_start = change.op.p + + if change.op.d? + # Shift any deletes after this along by the length of this insert + if op_start <= change_start + change.op.p += op_length + moved_changes.push change + else if change.op.i? + change_end = change_start + change.op.i.length + is_change_overlapping = (op_start >= change_start and op_start <= change_end) + + # If there is a delete at the start of the insert, and we're inserting + # at the start, we SHOULDN'T merge since the delete acts as a partition. + # The previous op will be the delete, but it's already been shifted by this insert + # + # I.e. + # Originally: |-- existing insert --| + # | <- existing delete at same offset + # + # Now: |-- existing insert --| <- not shifted yet + # |-- this insert --|| <- existing delete shifted along to end of this op + # + # After: |-- existing insert --| + # |-- this insert --|| <- existing delete + # + # Without the delete, the inserts would be merged. + is_insert_blocked_by_delete = (previous_change? and previous_change.op.d? and previous_change.op.p == op_end) + + # If the insert is overlapping another insert, either at the beginning in the middle or touching the end, + # then we merge them into one. + if is_change_overlapping and + !is_insert_blocked_by_delete and + !already_merged # With the way we order our changes, there should only ever be one candidate to merge + # with since changes don't overlap. However, this flag just adds a little bit of protection + offset = op_start - change_start + change.op.i = change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset) + already_merged = true + moved_changes.push change + else if op_start <= change_start + # If we're fully before the other insert we can just shift the other insert by our length. + # If they are touching, and should have been merged, they will have been above. + # If not merged above, then it must be blocked by a delete, and will be after this insert, so we shift it along as well + change.op.p += op_length + moved_changes.push change + previous_change = change + + if !already_merged + @_addOp op + + if moved_changes.length > 0 + @emit "changes:moved", moved_changes + + applyDelete: (op) -> + op_start = op.p + op_length = op.d.length + op_end = op.p + op_length + remove_changes = [] + moved_changes = [] + + # We might end up modifying our delete op if it merges with existing deletes, or cancels out + # with an existing insert. Since we might do multiple modifications, we record them and do + # all the modifications after looping through the existing changes, so as not to mess up the + # offset indexes as we go. + op_modifications = [] + for change in @changes + if change.op.i? + change_start = change.op.p + change_end = change_start + change.op.i.length + if op_end <= change_start + # Shift ops after us back by our length + change.op.p -= op_length + moved_changes.push change + else if op_start >= change_end + # Delete is after insert, nothing to do + else + # When the new delete overlaps an insert, we should remove the part of the insert that + # is now deleted, and also remove the part of the new delete that overlapped. I.e. + # the two cancel out where they overlap. + if op_start >= change_start + # |-- existing insert --| + # insert_remaining_before -> |.....||-- new delete --| + delete_remaining_before = "" + insert_remaining_before = change.op.i.slice(0, op_start - change_start) + else + # delete_remaining_before -> |.....||-- existing insert --| + # |-- new delete --| + delete_remaining_before = op.d.slice(0, change_start - op_start) + insert_remaining_before = "" + + if op_end <= change_end + # |-- existing insert --| + # |-- new delete --||.....| <- insert_remaining_after + delete_remaining_after = "" + insert_remaining_after = change.op.i.slice(op_end - change_start) + else + # |-- existing insert --||.....| <- delete_remaining_after + # |-- new delete --| + delete_remaining_after = op.d.slice(change_end - op_start) + insert_remaining_after = "" + + insert_remaining = insert_remaining_before + insert_remaining_after + if insert_remaining.length > 0 + change.op.i = insert_remaining + change.op.p = Math.min(change_start, op_start) + moved_changes.push change + else + remove_changes.push change + + # We know what we want to preserve of our delete op before (delete_remaining_before) and what we want to preserve + # afterwards (delete_remaining_before). Now we need to turn that into a modification which deletes the + # chunk in the middle not covered by these. + delete_removed_length = op.d.length - delete_remaining_before.length - delete_remaining_after.length + delete_removed_start = delete_remaining_before.length + modification = { + d: op.d.slice(delete_removed_start, delete_removed_start + delete_removed_length) + p: delete_removed_start + } + if modification.d.length > 0 + op_modifications.push modification + else if change.op.d? + change_start = change.op.p + if op_end < change_start + # Shift ops after us (but not touching) back by our length + change.op.p -= op_length + moved_changes.push change + else if op_start <= change_start <= op_end + # If we overlap a delete, add it in our content, and delete the existing change + offset = change_start - op_start + op_modifications.push { i: change.op.d, p: offset } + remove_changes.push change + + op.d = @_applyOpModifications(op.d, op_modifications) + if op.d.length > 0 + @_addOp op + + for change in remove_changes + @_removeChange change + + if moved_changes.length > 0 + @emit "changes:moved", moved_changes + + _newId: () -> + @id++ + + _addOp: (op) -> + change = { + id: @_newId() + op: op + } + @changes.push change + + # Keep ops in order of offset, with deletes before inserts + @changes.sort (c1, c2) -> + result = c1.op.p - c2.op.p + if result != 0 + return result + else if c1.op.i? and c2.op.d? + return 1 + else + return -1 + + if op.d? + @emit "delete:added", change + else if op.i? + @emit "insert:added", change + + _removeChange: (change) -> + @changes = @changes.filter (c) -> c.id != change.id + if change.op.d? + @emit "delete:removed", change + else if change.op.i? + @emit "insert:removed", change + + _applyOpModifications: (content, op_modifications) -> + # Put in descending position order, with deleting first if at the same offset + # (Inserting first would modify the content that the delete will delete) + op_modifications.sort (a, b) -> + result = b.p - a.p + if result != 0 + return result + else if a.i? and b.d? + return 1 + else + return -1 + + for modification in op_modifications + if modification.i? + content = content.slice(0, modification.p) + modification.i + content.slice(modification.p) + else if modification.d? + if content.slice(modification.p, modification.p + modification.d.length) != modification.d + throw new Error("deleted content does not match. content: #{JSON.stringify(content)}; modification: #{JSON.stringify(modification)}") + content = content.slice(0, modification.p) + content.slice(modification.p + modification.d.length) + return content + + return TrackChangesManager \ No newline at end of file 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 a0dcaa1367..5c8b82e5ed 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -44,6 +44,8 @@ define [ App.controller "NewDocModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> + $scope.validFileRegex = ide.validFileRegex + $scope.inputs = name: "name.tex" $scope.state = @@ -74,6 +76,8 @@ define [ App.controller "NewFolderModalController", [ "$scope", "ide", "$modalInstance", "$timeout", "parent_folder", ($scope, ide, $modalInstance, $timeout, parent_folder) -> + $scope.validFileRegex = ide.validFileRegex + $scope.inputs = name: "name" $scope.state = diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee index f0813cf03b..e6573bbe57 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeEntityController.coffee @@ -26,9 +26,23 @@ define [ $scope.startRenaming = () -> $scope.entity.renaming = true + invalidModalShowing = false $scope.finishRenaming = () -> - delete $scope.entity.renaming name = $scope.inputs.name + + if !name.match(new RegExp(ide.validFileRegex)) + # Showing the modal blurs the rename box which calls us again + # so track this with the invalidModalShowing flag + return if invalidModalShowing + invalidModalShowing = true + modal = $modal.open( + templateUrl: "invalidFileNameModalTemplate" + ) + modal.result.then () -> + invalidModalShowing = false + return + + delete $scope.entity.renaming if !name? or name.length == 0 $scope.inputs.name = $scope.entity.name return diff --git a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee similarity index 76% rename from services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee rename to services/web/public/coffee/ide/history/HistoryManager.coffee index 5270754db7..12de2149d8 100644 --- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/history/HistoryManager.coffee @@ -1,30 +1,31 @@ define [ - "ide/track-changes/controllers/TrackChangesListController" - "ide/track-changes/controllers/TrackChangesDiffController" - "ide/track-changes/directives/infiniteScroll" -], () -> - class TrackChangesManager + "moment" + "ide/history/controllers/HistoryListController" + "ide/history/controllers/HistoryDiffController" + "ide/history/directives/infiniteScroll" +], (moment) -> + class HistoryManager constructor: (@ide, @$scope) -> @reset() - @$scope.toggleTrackChanges = () => - if @$scope.ui.view == "track-changes" + @$scope.toggleHistory = () => + if @$scope.ui.view == "history" @hide() else @show() - @$scope.$watch "trackChanges.selection.updates", (updates) => + @$scope.$watch "history.selection.updates", (updates) => if updates? and updates.length > 0 @_selectDocFromUpdates() @reloadDiff() @$scope.$on "entity:selected", (event, entity) => - if (@$scope.ui.view == "track-changes") and (entity.type == "doc") - @$scope.trackChanges.selection.doc = entity + if (@$scope.ui.view == "history") and (entity.type == "doc") + @$scope.history.selection.doc = entity @reloadDiff() show: () -> - @$scope.ui.view = "track-changes" + @$scope.ui.view = "history" @reset() hide: () -> @@ -33,7 +34,7 @@ define [ @$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity() reset: () -> - @$scope.trackChanges = { + @$scope.history = { updates: [] nextBeforeTimestamp: null atEnd: false @@ -51,36 +52,36 @@ define [ } autoSelectRecentUpdates: () -> - return if @$scope.trackChanges.updates.length == 0 + return if @$scope.history.updates.length == 0 - @$scope.trackChanges.updates[0].selectedTo = true + @$scope.history.updates[0].selectedTo = true indexOfLastUpdateNotByMe = 0 - for update, i in @$scope.trackChanges.updates + for update, i in @$scope.history.updates if @_updateContainsUserId(update, @$scope.user.id) break indexOfLastUpdateNotByMe = i - @$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true + @$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" - if @$scope.trackChanges.nextBeforeTimestamp? - url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}" - @$scope.trackChanges.loading = true + if @$scope.history.nextBeforeTimestamp? + url += "&before=#{@$scope.history.nextBeforeTimestamp}" + @$scope.history.loading = true @ide.$http .get(url) .success (data) => @_loadUpdates(data.updates) - @$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp + @$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp if !data.nextBeforeTimestamp? - @$scope.trackChanges.atEnd = true - @$scope.trackChanges.loading = false + @$scope.history.atEnd = true + @$scope.history.loading = false reloadDiff: () -> - diff = @$scope.trackChanges.diff - {updates, doc} = @$scope.trackChanges.selection + diff = @$scope.history.diff + {updates, doc} = @$scope.history.selection {fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection() return if !doc? @@ -90,7 +91,7 @@ define [ diff.fromV == fromV and diff.toV == toV - @$scope.trackChanges.diff = diff = { + @$scope.history.diff = diff = { fromV: fromV toV: toV start_ts: start_ts @@ -117,6 +118,9 @@ define [ diff.error = true else diff.deleted = true + diff.restoreInProgress = false + diff.restoreDeletedSuccess = false + diff.restoredDocNewId = null restoreDeletedDoc: (doc) -> url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore" @@ -180,7 +184,7 @@ define [ return {text, highlights} _loadUpdates: (updates = []) -> - previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1] + previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1] for update in updates for doc_id, doc of update.docs or {} @@ -199,19 +203,19 @@ define [ previousUpdate = update - firstLoad = @$scope.trackChanges.updates.length == 0 + firstLoad = @$scope.history.updates.length == 0 - @$scope.trackChanges.updates = - @$scope.trackChanges.updates.concat(updates) + @$scope.history.updates = + @$scope.history.updates.concat(updates) @autoSelectRecentUpdates() if firstLoad _calculateRangeFromSelection: () -> fromV = toV = start_ts = end_ts = null - selected_doc_id = @$scope.trackChanges.selection.doc?.id + selected_doc_id = @$scope.history.selection.doc?.id - for update in @$scope.trackChanges.selection.updates or [] + for update in @$scope.history.selection.updates or [] for doc_id, doc of update.docs if doc_id == selected_doc_id if fromV? and toV? @@ -233,11 +237,11 @@ define [ # then prefer this one if present. _selectDocFromUpdates: () -> affected_docs = {} - for update in @$scope.trackChanges.selection.updates + for update in @$scope.history.selection.updates for doc_id, doc of update.docs affected_docs[doc_id] = doc.entity - selected_doc = @$scope.trackChanges.selection.doc + selected_doc = @$scope.history.selection.doc if selected_doc? and affected_docs[selected_doc.id]? # Selected doc is already open else @@ -245,7 +249,7 @@ define [ selected_doc = doc break - @$scope.trackChanges.selection.doc = selected_doc + @$scope.history.selection.doc = selected_doc @ide.fileTreeManager.selectEntity(selected_doc) _updateContainsUserId: (update, user_id) -> diff --git a/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee new file mode 100644 index 0000000000..d32155395e --- /dev/null +++ b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee @@ -0,0 +1,46 @@ +define [ + "base" +], (App) -> + App.controller "HistoryDiffController", ($scope, $modal, ide, event_tracking) -> + $scope.restoreDeletedDoc = () -> + event_tracking.sendMB "history-restore-deleted" + $scope.history.diff.restoreInProgress = true + ide.historyManager + .restoreDeletedDoc( + $scope.history.diff.doc + ) + .success (response) -> + $scope.history.diff.restoredDocNewId = response.doc_id + $scope.history.diff.restoreInProgress = false + $scope.history.diff.restoreDeletedSuccess = true + + $scope.openRestoreDiffModal = () -> + event_tracking.sendMB "history-restore-modal" + $modal.open { + templateUrl: "historyRestoreDiffModalTemplate" + controller: "HistoryRestoreDiffModalController" + resolve: + diff: () -> $scope.history.diff + } + + $scope.backToEditorAfterRestore = () -> + ide.editorManager.openDoc({ id: $scope.history.diff.restoredDocNewId }) + + App.controller "HistoryRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) -> + $scope.state = + inflight: false + + $scope.diff = diff + + $scope.restore = () -> + event_tracking.sendMB "history-restored" + $scope.state.inflight = true + ide.historyManager + .restoreDiff(diff) + .success () -> + $scope.state.inflight = false + $modalInstance.close() + ide.editorManager.openDoc(diff.doc) + + $scope.cancel = () -> + $modalInstance.dismiss() diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee similarity index 71% rename from services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee rename to services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index 8172b00727..d3ca0c50f2 100644 --- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -2,26 +2,26 @@ define [ "base" ], (App) -> - App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)-> + App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)-> $scope.$watch "ui.view", -> - if $scope.ui.view == "track-changes" + if $scope.ui.view == "history" if $scope.project?.features?.versioning $scope.versioningPopupType = "default" - else if $scope.ui.view == "track-changes" - sixpack.participate 'track-changes-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> + else if $scope.ui.view == "history" + sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> $scope.versioningPopupType = chosenVariation - App.controller "TrackChangesListController", ["$scope", "ide", ($scope, ide) -> + App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> $scope.hoveringOverListSelectors = false $scope.loadMore = () => - ide.trackChangesManager.fetchNextBatchOfUpdates() + ide.historyManager.fetchNextBatchOfUpdates() $scope.recalculateSelectedUpdates = () -> beforeSelection = true afterSelection = false - $scope.trackChanges.selection.updates = [] - for update in $scope.trackChanges.updates + $scope.history.selection.updates = [] + for update in $scope.history.updates if update.selectedTo inSelection = true beforeSelection = false @@ -31,7 +31,7 @@ define [ update.afterSelection = afterSelection if inSelection - $scope.trackChanges.selection.updates.push update + $scope.history.selection.updates.push update if update.selectedFrom inSelection = false @@ -40,7 +40,7 @@ define [ $scope.recalculateHoveredUpdates = () -> hoverSelectedFrom = false hoverSelectedTo = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates # Figure out whether the to or from selector is hovered over if update.hoverSelectedFrom hoverSelectedFrom = true @@ -50,7 +50,7 @@ define [ if hoverSelectedFrom # We want to 'hover select' everything between hoverSelectedFrom and selectedTo inHoverSelection = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates if update.selectedTo update.hoverSelectedTo = true inHoverSelection = true @@ -60,7 +60,7 @@ define [ if hoverSelectedTo # We want to 'hover select' everything between hoverSelectedTo and selectedFrom inHoverSelection = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates if update.hoverSelectedTo inHoverSelection = true update.inHoverSelection = inHoverSelection @@ -69,48 +69,49 @@ define [ inHoverSelection = false $scope.resetHoverState = () -> - for update in $scope.trackChanges.updates + for update in $scope.history.updates delete update.hoverSelectedFrom delete update.hoverSelectedTo delete update.inHoverSelection - $scope.$watch "trackChanges.updates.length", () -> + $scope.$watch "history.updates.length", () -> $scope.recalculateSelectedUpdates() ] - App.controller "TrackChangesListItemController", ["$scope", ($scope) -> + App.controller "HistoryListItemController", ["$scope", "event_tracking", ($scope, event_tracking) -> $scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) -> if selectedFrom - for update in $scope.trackChanges.updates + for update in $scope.history.updates update.selectedFrom = false unless update == $scope.update $scope.recalculateSelectedUpdates() $scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) -> if selectedTo - for update in $scope.trackChanges.updates + for update in $scope.history.updates update.selectedTo = false unless update == $scope.update $scope.recalculateSelectedUpdates() $scope.select = () -> + event_tracking.sendMB "history-view-change" $scope.update.selectedTo = true $scope.update.selectedFrom = true $scope.mouseOverSelectedFrom = () -> - $scope.trackChanges.hoveringOverListSelectors = true + $scope.history.hoveringOverListSelectors = true $scope.update.hoverSelectedFrom = true $scope.recalculateHoveredUpdates() $scope.mouseOutSelectedFrom = () -> - $scope.trackChanges.hoveringOverListSelectors = false + $scope.history.hoveringOverListSelectors = false $scope.resetHoverState() $scope.mouseOverSelectedTo = () -> - $scope.trackChanges.hoveringOverListSelectors = true + $scope.history.hoveringOverListSelectors = true $scope.update.hoverSelectedTo = true $scope.recalculateHoveredUpdates() $scope.mouseOutSelectedTo = () -> - $scope.trackChanges.hoveringOverListSelectors = false + $scope.history.hoveringOverListSelectors = false $scope.resetHoverState() $scope.displayName = (user) -> diff --git a/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee b/services/web/public/coffee/ide/history/directives/infiniteScroll.coffee similarity index 100% rename from services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee rename to services/web/public/coffee/ide/history/directives/infiniteScroll.coffee diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee index 36eaa5d555..18d075c094 100644 --- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee +++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee @@ -4,7 +4,7 @@ define [ ], (App) -> App.controller "HotkeysController", ($scope, $modal, event_tracking) -> $scope.openHotkeysModal = -> - event_tracking.sendCountly "ide-open-hotkeys-modal" + event_tracking.sendMB "ide-open-hotkeys-modal" $modal.open { templateUrl: "hotkeysModalTemplate" diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee index 747ca1ad08..61cc1b7df7 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee @@ -3,18 +3,40 @@ define [ "ide/human-readable-logs/HumanReadableLogsRules" ], (LogParser, ruleset) -> parse : (rawLog, options) -> - parsedLogEntries = LogParser.parse(rawLog, options) + if typeof rawLog is 'string' + parsedLogEntries = LogParser.parse(rawLog, options) + else + parsedLogEntries = rawLog _getRule = (logMessage) -> return rule for rule in ruleset when rule.regexToMatch.test logMessage + seenErrorTypes = {} # keep track of types of errors seen + for entry in parsedLogEntries.all ruleDetails = _getRule entry.message if (ruleDetails?) - entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch? + if ruleDetails.ruleId? + entry.ruleId = ruleDetails.ruleId + else if ruleDetails.regexToMatch? + entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) + if ruleDetails.newMessage? + entry.message = entry.message.replace ruleDetails.regexToMatch, ruleDetails.newMessage + # suppress any entries that are known to cascade from previous error types + if ruleDetails.cascadesFrom? + for type in ruleDetails.cascadesFrom + entry.suppressed = true if seenErrorTypes[type] + # record the types of errors seen + if ruleDetails.types? + for type in ruleDetails.types + seenErrorTypes[type] = true entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint? entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? - + + # filter out the suppressed errors (from the array entries in parsedLogEntries) + for key, errors of parsedLogEntries when typeof errors is 'object' and errors.length > 0 + parsedLogEntries[key] = (err for err in errors when not err.suppressed) + return parsedLogEntries diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee index f123515deb..502f77fb98 100644 --- a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -20,7 +20,7 @@ define -> [ regexToMatch: /Missing [{$] inserted./ extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_$_inserted" humanReadableHint: """ - Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list here.If you intended to use mathematics mode, then use $ \u2026 $ for 'inline math mode', $$ \u2026 $$ for 'display math mode' or alternatively \begin{math} \u2026 \end{math}. + Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list here.If you intended to use mathematics mode, then use $ \u2026 $ for 'inline math mode', $$ \u2026 $$ for 'display math mode' or alternatively \\begin{math} \u2026 \\end{math}. """ , regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/ @@ -88,4 +88,125 @@ define -> [ humanReadableHint: """ You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc. """ + , + ruleId: "hint_mismatched_environment" + types: ['environment'] + regexToMatch: /Error: `([^']{2,})' expected, found `([^']{2,})'.*/ + newMessage: "Error: environment does not match \\begin{$1} ... \\end{$2}" + humanReadableHint: """ + You have used \\begin{...} without a corresponding \\end{...}. + """ + , + ruleId: "hint_mismatched_brackets" + types: ['environment'] + regexToMatch: /Error: `([^a-zA-Z0-9])' expected, found `([^a-zA-Z0-9])'.*/ + newMessage: "Error: brackets do not match, found '$2' instead of '$1'" + humanReadableHint: """ + You have used an open bracket without a corresponding close bracket. + """ + , + regexToMatch: /LaTeX Error: Can be used only in preamble/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Can_be_used_only_in_preamble" + humanReadableHint: """ + You have used a command in the main body of your document which should be used in the preamble. Make sure that \\documentclass[\u2026]{\u2026} and all \\usepackage{\u2026} commands are written before \\begin{document}. + """ + , + regexToMatch: /Missing \\right inserted/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_%5Cright_insertede" + humanReadableHint: """ + You have started an expression with a \\left command, but have not included a corresponding \\right command. Make sure that your \\left and \\right commands balance everywhere, or else try using \\Biggl and \\Biggr commands instead as shown here. + """ + , + regexToMatch: /Double superscript/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_superscript" + humanReadableHint: """ + You have written a double superscript incorrectly as a^b^c, or else you have written a prime with a superscript. Remember to include { and } when using multiple superscripts. Try a^{b^c} instead. + """ + , + regexToMatch: /Double subscript/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Double_subscript" + humanReadableHint: """ + You have written a double subscript incorrectly as a_b_c. Remember to include { and } when using multiple subscripts. Try a_{b_c} instead. + """ + , + regexToMatch: /No \\author given/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_%5Cauthor_given" + humanReadableHint: """ + You have used the \\maketitle command, but have not specified any \\author. To fix this, include an author in your preamble using the \\author{\u2026} command. + """ + , + regexToMatch: /LaTeX Error: Environment .+ undefined/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Environment%20XXX%20undefined" + humanReadableHint: """ + You have created an environment (using \\begin{\u2026} and \\end{\u2026} commands) which is not recognized. Make sure you have included the required package for that environment in your preamble, and that the environment is spelled correctly. + """ + , + regexToMatch: /LaTeX Error: Something's wrong--perhaps a missing \\item/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Something%27s_wrong--perhaps_a_missing_%5Citem" + humanReadableHint: """ + There are no entries found in a list you have created. Make sure you label list entries using the \\item command, and that you have not used a list inside a table. + """ + , + regexToMatch: /Misplaced \\noalign/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_%5Cnoalign" + humanReadableHint: """ + You have used a \\hline command in the wrong place, probably outside a table. If the \\hline command is written inside a table, try including \\\ before it. + """ + , + regexToMatch: /LaTeX Error: There's no line here to end/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_There%27s_no_line_here_to_end" + humanReadableHint: """ + You have used a \\\\ or \\newline command where LaTeX was not expecting one. Make sure that you only use line breaks after blocks of text, and be careful using linebreaks inside lists and other environments. + """ + , + regexToMatch: /LaTeX Error: \\verb ended by end of line/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cverb_ended_by_end_of_line" + humanReadableHint: """ + You have used a \\verb command incorrectly. Try replacling the \\verb command with \begin{verbatim}\u2026\end{verbatim}. + """ + , + regexToMatch: /Illegal unit of measure (pt inserted)/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FIllegal%20unit%20of%20measure%20(pt%20inserted)" + humanReadableHint: """ + You have written a length, but have not specified the appropriate units (pt, mm, cm etc.). If you have not written a length, check that you have not witten a linebreak \\\\ followed by square brackets [\u2026] anywhere. + """ + , + regexToMatch: /Extra \\right/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_%5Cright" + humanReadableHint: """ + You have written a \\right command without a corresponding \\left command. Check that all \\left and \\right commands balance everywhere. + """ + , + regexToMatch: /Missing \\begin{document}/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors%2FLaTeX%20Error%3A%20Missing%20%5Cbegin%20document" + humanReadableHint: """ + No \\begin{document} command was found. Make sure you have included \\begin{document} in your preamble, and that your main document is set correctly. + """ + , + ruleId: "hint_mismatched_environment2" + types: ['environment'] + cascadesFrom: ['environment'] + regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/ + newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}" + humanReadableHint: """ + You have used \\begin{} without a corresponding \\end{}. + """ + , + ruleId: "hint_mismatched_environment3" + types: ['environment'] + cascadesFrom: ['environment'] + regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/ + newMessage: "Warning: No matching \\end found for \\begin{$1}" + humanReadableHint: """ + You have used \\begin{} without a corresponding \\end{}. + """ + , + ruleId: "hint_mismatched_environment4" + types: ['environment'] + cascadesFrom: ['environment'] + regexToMatch: /Error: Found `\\end\{([^\}]+)\}' without corresponding \\begin.*/ + newMessage: "Error: found \\end{$1} without a corresponding \\begin{$1}" + humanReadableHint: """ + You have used \\begin{} without a corresponding \\end{}. + """ ] diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 0f8134258c..790f2384a1 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -36,11 +36,11 @@ define [ $scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts $scope.trackLogHintsLearnMore = () -> - event_tracking.sendCountly "logs-hints-learn-more" + event_tracking.sendMB "logs-hints-learn-more" trackLogHintsFeedback = (isPositive, hintId) -> event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId - event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId } + event_tracking.sendMB (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId } $scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) -> logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal @@ -73,6 +73,13 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.renderingError = true + # abort compile if syntax checks fail + $scope.stop_on_validation_error = localStorage("stop_on_validation_error:#{$scope.project_id}") + $scope.stop_on_validation_error ?= true # turn on for all users by default + $scope.$watch "stop_on_validation_error", (new_value, old_value) -> + if new_value? and old_value != new_value + localStorage("stop_on_validation_error:#{$scope.project_id}", new_value) + $scope.draft = localStorage("draft:#{$scope.project_id}") or false $scope.$watch "draft", (new_value, old_value) -> if new_value? and old_value != new_value @@ -83,14 +90,29 @@ define [ params = {} if options.isAutoCompile params["auto_compile"]=true + # if the previous run was a check, clear the error logs + $scope.pdf.logEntries = [] if $scope.check + # keep track of whether this is a compile or check + $scope.check = if options.check then true else false + event_tracking.sendMB "syntax-check-request" if options.check + # send appropriate check type to clsi + checkType = switch + when $scope.check then "validate" # validate only + when options.try then "silent" # allow use to try compile once + when $scope.stop_on_validation_error then "error" # try to compile + else "silent" # ignore errors return $http.post url, { rootDoc_id: options.rootDocOverride_id or null draft: $scope.draft + check: checkType _csrf: window.csrfToken }, {params: params} parseCompileResponse = (response) -> + # keep last url + last_pdf_url = $scope.pdf.url + # Reset everything $scope.pdf.error = false $scope.pdf.timedout = false @@ -101,6 +123,8 @@ define [ $scope.pdf.renderingError = false $scope.pdf.projectTooLarge = false $scope.pdf.compileTerminated = false + $scope.pdf.compileExited = false + $scope.pdf.failedCheck = false # make a cache to look up files by name fileByPath = {} @@ -120,11 +144,24 @@ define [ if response.status == "timedout" $scope.pdf.view = 'errors' $scope.pdf.timedout = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) else if response.status == "terminated" $scope.pdf.view = 'errors' $scope.pdf.compileTerminated = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) + else if response.status in ["validation-fail", "validation-pass"] + $scope.pdf.view = 'pdf' + $scope.pdf.url = last_pdf_url + $scope.shouldShowLogs = true + $scope.pdf.failedCheck = true if response.status is "validation-fail" + event_tracking.sendMB "syntax-check-#{response.status}" + fetchLogs(fileByPath, { validation: true }) + else if response.status == "exited" + $scope.pdf.view = 'pdf' + $scope.pdf.compileExited = true + $scope.pdf.url = last_pdf_url + $scope.shouldShowLogs = true + fetchLogs(fileByPath) else if response.status == "autocompile-backoff" $scope.pdf.view = 'uncompiled' else if response.status == "project-too-large" @@ -134,7 +171,7 @@ define [ $scope.pdf.view = 'errors' $scope.pdf.failure = true $scope.shouldShowLogs = true - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) else if response.status == 'clsi-maintenance' $scope.pdf.view = 'errors' $scope.pdf.clsiMaintenance = true @@ -165,7 +202,7 @@ define [ qs.popupDownload = true $scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs) - fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + fetchLogs(fileByPath) IGNORE_FILES = ["output.fls", "output.fdb_latexmk"] $scope.pdf.outputFiles = [] @@ -179,14 +216,25 @@ define [ qs.clsiserverid = response.clsiServerId for file in response.outputFiles if IGNORE_FILES.indexOf(file.path) == -1 + isOutputFile = file.path.match(/^output\./) $scope.pdf.outputFiles.push { # Turn 'output.blg' into 'blg file'. - name: if file.path.match(/^output\./) then "#{file.path.replace(/^output\./, "")} file" else file.path + name: if isOutputFile then "#{file.path.replace(/^output\./, "")} file" else file.path url: "/project/#{project_id}/output/#{file.path}" + createQueryString qs + main: if isOutputFile then true else false } + # sort the output files into order, main files first, then others + $scope.pdf.outputFiles.sort (a,b) -> (b.main - a.main) || a.name.localeCompare(b.name) - fetchLogs = (logFile, blgFile) -> + + fetchLogs = (fileByPath, options) -> + + if options?.validation + chktexFile = fileByPath['output.chktex'] + else + logFile = fileByPath['output.log'] + blgFile = fileByPath['output.blg'] getFile = (name, file) -> opts = @@ -213,6 +261,8 @@ define [ accumulateResults = (newEntries) -> for key in ['all', 'errors', 'warnings'] + if newEntries.type? + entry.type = newEntries.type for entry in newEntries[key] logEntries[key] = logEntries[key].concat newEntries[key] # use the parsers for each file type @@ -222,10 +272,25 @@ define [ all = [].concat errors, warnings, typesetting accumulateResults {all, errors, warnings} + processChkTex = (log) -> + errors = [] + warnings = [] + for line in log.split("\n") + if m = line.match /^(\S+):(\d+):(\d+): (Error|Warning): (.*)/ + result = { file:m[1], line:m[2], column:m[3], level:m[4].toLowerCase(), message: "#{m[4]}: #{m[5]}"} + if result.level is 'error' + errors.push result + else + warnings.push result + all = [].concat errors, warnings + logHints = HumanReadableLogs.parse {type: "Syntax", all, errors, warnings} + event_tracking.sendMB "syntax-check-return-count", {errors:errors.length, warnings:warnings.length} + accumulateResults logHints + processBiber = (log) -> {errors, warnings} = BibLogParser.parse(log, {}) all = [].concat errors, warnings - accumulateResults {all, errors, warnings} + accumulateResults {type: "BibTeX", all, errors, warnings} # output the results handleError = () -> @@ -248,19 +313,35 @@ define [ } # retrieve the logfile and process it - response = getFile('output.log', logFile) - .success processLog - .error handleError + if logFile? + response = getFile('output.log', logFile) + .then (response) -> processLog(response.data) - if blgFile? # retrieve the blg file if present - response.success () -> - getFile('output.blg', blgFile) - # ignore errors in biber file - .success processBiber - # display the combined result - .then annotateFiles - else # otherwise just display the result - response.success annotateFiles + if blgFile? # retrieve the blg file if present + response = response.then () -> + getFile('output.blg', blgFile) + .then( + (response) -> processBiber(response.data), + () -> true # ignore errors in biber file + ) + + if response? + response.catch handleError + else + handleError() + + if chktexFile? + getChkTex = () -> + getFile('output.chktex', chktexFile) + .then (response) -> processChkTex(response.data) + # always retrieve the chktex file if present + if response? + response = response.then getChkTex, getChkTex + else + response = getChkTex() + + # display the combined result + response.finally annotateFiles getRootDocOverride_id = () -> doc = ide.editorManager.getCurrentDocValue() @@ -284,10 +365,20 @@ define [ $scope.recompile = (options = {}) -> return if $scope.pdf.compiling - event_tracking.sendCountlySampled "editor-recompile-sampled", options + event_tracking.sendMBSampled "editor-recompile-sampled", options $scope.pdf.compiling = true + if options?.force + # for forced compile, turn off validation check and ignore errors + $scope.stop_on_validation_error = false + $scope.shouldShowLogs = false # hide the logs while compiling + event_tracking.sendMB "syntax-check-turn-off-checking" + + if options?.try + $scope.shouldShowLogs = false # hide the logs while compiling + event_tracking.sendMB "syntax-check-try-compile-anyway" + ide.$scope.$broadcast("flush-changes") options.rootDocOverride_id = getRootDocOverride_id() @@ -333,7 +424,7 @@ define [ $scope.toggleLogs = () -> $scope.shouldShowLogs = !$scope.shouldShowLogs - event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs + event_tracking.sendMBOnce "ide-open-logs-once" if $scope.shouldShowLogs $scope.showPdf = () -> $scope.pdf.view = "pdf" @@ -341,7 +432,7 @@ define [ $scope.toggleRawLog = () -> $scope.pdf.showRawLog = !$scope.pdf.showRawLog - event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog + event_tracking.sendMB "logs-view-raw" if $scope.pdf.showRawLog $scope.openClearCacheModal = () -> modalInstance = $modal.open( @@ -376,7 +467,7 @@ define [ $scope.startFreeTrial = (source) -> ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source) - event_tracking.sendCountly "subscription-start-trial", { source } + event_tracking.sendMB "subscription-start-trial", { source } window.open("/user/subscription/new?planCode=student_free_trial_7_days") $scope.startedFreeTrial = true @@ -499,12 +590,14 @@ define [ App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) -> $scope.openInEditor = (entry) -> - event_tracking.sendCountlyOnce "logs-jump-to-location-once" + event_tracking.sendMBOnce "logs-jump-to-location-once" entity = ide.fileTreeManager.findEntityByPath(entry.file) return if !entity? or entity.type != "doc" if entry.line? line = entry.line - ide.editorManager.openDoc(entity, gotoLine: line) + if entry.column? + column = entry.column + ide.editorManager.openDoc(entity, gotoLine: line, gotoColumn: column) ] App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) -> diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee index f1c161ffc6..37f991eb84 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee @@ -6,9 +6,6 @@ define [ pdfViewer ) -> - if PDFJS? - PDFJS.workerSrc = window.pdfJsWorkerPath - App.directive "pdfng", ["$timeout", "localStorage", ($timeout, localStorage) -> return { scope: { diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee index d0f2f8a3ae..e0b6da3659 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee @@ -1,24 +1,46 @@ define [ "base" -], (App) -> + "pdfjs-dist/build/pdf" +], (App, PDFJS) -> # App = angular.module 'PDFRenderer', ['pdfAnnotations', 'pdfTextLayer'] App.factory 'PDFRenderer', ['$q', '$timeout', 'pdfAnnotations', 'pdfTextLayer', 'pdfSpinner', ($q, $timeout, pdfAnnotations, pdfTextLayer, pdfSpinner) -> + # Have a single worker used by all rendering, to avoid reloading + RenderThread = { worker: null, count: 0} + + getRenderThread = () -> + if RenderThread.count > 16 # recycle the worker periodically to avoid leaks + RenderThread.readyToDestroy = true + RenderThread = { worker: null, count: 0 } + RenderThread.worker ||= new PDFJS.PDFWorker('pdfjsworker') + RenderThread.count++ + return RenderThread + + resetWorker = (thread) -> + thread.worker.destroy() if thread.readyToDestroy + + # The PDF page renderer + class PDFRenderer JOB_QUEUE_INTERVAL: 25 PAGE_LOAD_TIMEOUT: 60*1000 INDICATOR_DELAY1: 100 # time to delay before showing the indicator INDICATOR_DELAY2: 250 # time until the indicator starts animating + TEXTLAYER_TIMEOUT: 100 constructor: (@url, @options) -> - # PDFJS.disableFontFace = true # avoids repaints, uses worker more + if window.location?.search?.indexOf("disable-font-face=true") >= 0 + window.PDFJS.disableFontFace = true + else + window.PDFJS.disableFontFace = false if @options.disableAutoFetch - PDFJS.disableAutoFetch = true # prevent loading whole file + window.PDFJS.disableAutoFetch = true # prevent loading whole file # PDFJS.disableStream # PDFJS.disableRange @scale = @options.scale || 1 - @pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536} + @thread = getRenderThread() + @pdfjs = PDFJS.getDocument {url: @url, rangeChunkSize: 2*65536, worker: @thread.worker} @pdfjs.onProgress = @options.progressCallback @document = $q.when(@pdfjs) @navigateFn = @options.navigateFn @@ -243,6 +265,12 @@ define [ return canvas = $('') + # In Windows+IE we must have the canvas in the DOM during + # rendering to see the fonts defined in the DOM. If we try to + # render 'offscreen' then all the text will be sans-serif. + # Previously we rendered offscreen and added in the canvas + # when rendering was complete. + element.canvas.replaceWith(canvas) viewport = page.getViewport (scale) @@ -280,6 +308,7 @@ define [ textLayer = new pdfTextLayer({ textLayerDiv: element.text[0] viewport: viewport + renderer: PDFJS.renderTextLayer }) annotationsLayer = new pdfAnnotations({ @@ -294,12 +323,14 @@ define [ transform: [pixelRatio, 0, 0, pixelRatio, 0, 0] } + textLayerTimeout = @TEXTLAYER_TIMEOUT + result.then () -> # page render success - element.canvas.replaceWith(canvas) canvas.removeClass('pdfng-rendering') - page.getTextContent().then (textContent) -> + page.getTextContent({normalizeWhitespace: true}).then (textContent) -> textLayer.setTextContent textContent + textLayer.render(textLayerTimeout) , (error) -> self.errorCallback?(error) page.getAnnotations().then (annotations) -> @@ -318,8 +349,9 @@ define [ destroy: () -> @shuttingDown = true @resetState() - @pdfjs.then (document) -> + @pdfjs.then (document) => document.cleanup() document.destroy() + resetWorker(@thread) ] diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee index 869b292352..4cd326c848 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee @@ -1,225 +1,57 @@ define [ "base" ], (App) -> - # App = angular.module 'pdfTextLayer', [] + + # uses the PDFJS text layer renderer to provide invisible overlayed + # text for searching App.factory 'pdfTextLayer', [ () -> - # TRANSLATED FROM pdf.js-1.0.712 - # pdf.js-1.0.712/web/ui_utils.js - # pdf.js-1.0.712/web/text_layer_builder.js - - # -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- - - # Copyright 2012 Mozilla Foundation - # * - # * Licensed under the Apache License, Version 2.0 (the "License"); - # * you may not use this file except in compliance with the License. - # * You may obtain a copy of the License at - # * - # * http://www.apache.org/licenses/LICENSE-2.0 - # * - # * Unless required by applicable law or agreed to in writing, software - # * distributed under the License is distributed on an "AS IS" BASIS, - # * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - # * See the License for the specific language governing permissions and - # * limitations under the License. - # - - # globals CustomStyle, scrollIntoView, PDFJS - # ms - - # optimised CSS custom property getter/setter - - CustomStyle = (CustomStyleClosure = -> - - # As noted on: http://www.zachstronaut.com/posts/2009/02/17/ - # animate-css-transforms-firefox-webkit.html - # in some versions of IE9 it is critical that ms appear in this list - # before Moz - CustomStyle = -> - prefixes = [ - 'ms' - 'Moz' - 'Webkit' - 'O' - ] - _cache = {} - CustomStyle.getProp = get = (propName, element) -> - - # check cache only when no element is given - return _cache[propName] if arguments.length is 1 and typeof _cache[propName] is 'string' - element = element or document.documentElement - style = element.style - prefixed = undefined - uPropName = undefined - - # test standard property first - return (_cache[propName] = propName) if typeof style[propName] is 'string' - - # capitalize - uPropName = propName.charAt(0).toUpperCase() + propName.slice(1) - - # test vendor specific properties - i = 0 - l = prefixes.length - - while i < l - prefixed = prefixes[i] + uPropName - return (_cache[propName] = prefixed) if typeof style[prefixed] is 'string' - i++ - - #if all fails then set to undefined - _cache[propName] = 'undefined' - - CustomStyle.setProp = set = (propName, element, str) -> - prop = @getProp(propName) - element.style[prop] = str if prop isnt 'undefined' - return - - CustomStyle - )() - - ################################# - - isAllWhitespace = (str) -> - not NonWhitespaceRegexp.test(str) - 'use strict' - FIND_SCROLL_OFFSET_TOP = -50 - FIND_SCROLL_OFFSET_LEFT = -400 - MAX_TEXT_DIVS_TO_RENDER = 100000 - RENDER_DELAY = 200 - NonWhitespaceRegexp = /\S/ - - ###* - TextLayerBuilder provides text-selection functionality for the PDF. - It does this by creating overlay divs over the PDF text. These divs - contain text that matches the PDF text they are overlaying. This object - also provides a way to highlight text that is being searched for. - ### - class pdfTextLayer constructor: (options) -> @textLayerDiv = options.textLayerDiv - @layoutDone = false @divContentDone = false - @pageIdx = options.pageIndex - @matches = [] - @lastScrollSource = options.lastScrollSource or null @viewport = options.viewport - @isViewerInPresentationMode = options.isViewerInPresentationMode @textDivs = [] - @findController = options.findController or null + @renderer = options.renderer + @renderingDone = false - renderLayer: () -> + render: (timeout) -> + if @renderingDone or not @divContentDone + return + + if @textLayerRenderTask? + @textLayerRenderTask.cancel() + @textLayerRenderTask = null + + @textDivs = [] textLayerFrag = document.createDocumentFragment() - textDivs = @textDivs - textDivsLength = textDivs.length - canvas = document.createElement('canvas') - ctx = canvas.getContext('2d') - # No point in rendering many divs as it would make the browser - # unusable even after the divs are rendered. - return if textDivsLength > MAX_TEXT_DIVS_TO_RENDER - lastFontSize = undefined - lastFontFamily = undefined - i = 0 - while i < textDivsLength - textDiv = textDivs[i] - if textDiv.dataset.isWhitespace - i++ - continue - fontSize = textDiv.style.fontSize - fontFamily = textDiv.style.fontFamily + @textLayerRenderTask = @renderer { + textContent: this.textContent, + container: textLayerFrag, + viewport: this.viewport, + textDivs: this.textDivs, + timeout: timeout, + enhanceTextSelection: this.enhanceTextSelection, + } - # Only build font string and set to context if different from last. - if fontSize isnt lastFontSize or fontFamily isnt lastFontFamily - ctx.font = fontSize + ' ' + fontFamily - lastFontSize = fontSize - lastFontFamily = fontFamily - width = ctx.measureText(textDiv.textContent).width - if width > 0 - textLayerFrag.appendChild textDiv + textLayerSuccess = () => + @textLayerDiv.appendChild(textLayerFrag) + @renderingDone = true - if textDiv.dataset.canvasWidth? - # Dataset values come of type string. - textScale = textDiv.dataset.canvasWidth / width; - transform = 'scaleX(' + textScale + ')' - else - transform = '' - rotation = textDiv.dataset.angle - if rotation - transform = 'rotate(' + rotation + 'deg) ' + transform - if transform - CustomStyle.setProp 'transform', textDiv, transform - i++ - @textLayerDiv.appendChild textLayerFrag - return + textLayerFailure = () -> + return # canceled or failed to render text layer -- skipping errors - appendText: (geom, styles) -> - style = styles[geom.fontName] - textDiv = document.createElement('div') - @textDivs.push textDiv - if isAllWhitespace(geom.str) - textDiv.dataset.isWhitespace = true - return - tx = PDFJS.Util.transform(@viewport.transform, geom.transform) - angle = Math.atan2(tx[1], tx[0]) - angle += Math.PI / 2 if style.vertical - fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])) - fontAscent = fontHeight - if style.ascent - fontAscent = style.ascent * fontAscent - else fontAscent = (1 + style.descent) * fontAscent if style.descent - left = undefined - top = undefined - if angle is 0 - left = tx[4] - top = tx[5] - fontAscent - else - left = tx[4] + (fontAscent * Math.sin(angle)) - top = tx[5] - (fontAscent * Math.cos(angle)) - textDiv.style.left = left + 'px' - textDiv.style.top = top + 'px' - textDiv.style.fontSize = fontHeight + 'px' - textDiv.style.fontFamily = style.fontFamily - textDiv.textContent = geom.str - - textDiv.ondblclick = (e) -> - if (window.getSelection) - window.getSelection().removeAllRanges(); - else if (document.selection) - document.selection.empty(); - - # |fontName| is only used by the Font Inspector. This test will succeed - # when e.g. the Font Inspector is off but the Stepper is on, but it's - # not worth the effort to do a more accurate test. - textDiv.dataset.fontName = geom.fontName if PDFJS.pdfBug - - # Storing into dataset will convert number into string. - textDiv.dataset.angle = angle * (180 / Math.PI) if angle isnt 0 - # We don't bother scaling single-char text divs, because it has very - # little effect on text highlighting. This makes scrolling on docs with - # lots of such divs a lot faster. - if textDiv.textContent.length > 1 - if style.vertical - textDiv.dataset.canvasWidth = geom.height * @viewport.scale - else - textDiv.dataset.canvasWidth = geom.width * @viewport.scale - return + @textLayerRenderTask.promise.then(textLayerSuccess, textLayerFailure) setTextContent: (textContent) -> - @textContent = textContent - textItems = textContent.items - i = 0 - len = textItems.length + if (@textLayerRenderTask) + @textLayerRenderTask.cancel(); + @textLayerRenderTask = null; - while i < len - @appendText textItems[i], textContent.styles - i++ - @divContentDone = true - @renderLayer() - return + @textContent = textContent; + @divContentDone = true; ] diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee index 80306771df..ff4d5a4101 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee @@ -6,7 +6,6 @@ define [ "ide/pdfng/directives/pdfRenderer" "ide/pdfng/directives/pdfPage" "ide/pdfng/directives/pdfSpinner" - "libs/pdf" ], ( App pdfTextLayer diff --git a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee index c032ca20ce..29bc8e979c 100644 --- a/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee +++ b/services/web/public/coffee/ide/settings/controllers/SettingsController.coffee @@ -28,6 +28,10 @@ define [ if pdfViewer != oldPdfViewer settings.saveSettings({pdfViewer: pdfViewer}) + $scope.$watch "settings.syntaxValidation", (syntaxValidation, oldSyntaxValidation) => + if syntaxValidation != oldSyntaxValidation + settings.saveProjectSettings({syntaxValidation: syntaxValidation}) + $scope.$watch "project.spellCheckLanguage", (language, oldLanguage) => return if @ignoreUpdates if oldLanguage? and language != oldLanguage diff --git a/services/web/public/coffee/ide/settings/services/settings.coffee b/services/web/public/coffee/ide/settings/services/settings.coffee index 14dfe92f56..04a9ccb5e3 100644 --- a/services/web/public/coffee/ide/settings/services/settings.coffee +++ b/services/web/public/coffee/ide/settings/services/settings.coffee @@ -8,7 +8,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal } + event_tracking.sendMB "setting-changed", { changedSetting, changedSettingVal } # End of tracking code. data._csrf = window.csrfToken @@ -20,7 +20,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal} + event_tracking.sendMB "project-setting-changed", { changedSetting, changedSettingVal} # End of tracking code. data._csrf = window.csrfToken @@ -32,7 +32,7 @@ define [ for key in Object.keys(data) changedSetting = key changedSettingVal = data[key] - event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal } + event_tracking.sendMB "project-admin-setting-changed", { changedSetting, changedSettingVal } # End of tracking code. data._csrf = window.csrfToken diff --git a/services/web/public/coffee/ide/share/controllers/ShareController.coffee b/services/web/public/coffee/ide/share/controllers/ShareController.coffee index 2378391974..9fdf4d31e2 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareController.coffee @@ -1,13 +1,30 @@ define [ "base" ], (App) -> - App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) -> - $scope.openShareProjectModal = () -> - event_tracking.sendCountlyOnce "ide-open-share-modal-once" + App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking", + ($scope, $modal, ide, projectInvites, projectMembers, event_tracking) -> + $scope.openShareProjectModal = () -> + event_tracking.sendMBOnce "ide-open-share-modal-once" - $modal.open( - templateUrl: "shareProjectModalTemplate" - controller: "ShareProjectModalController" - scope: $scope - ) + $modal.open( + templateUrl: "shareProjectModalTemplate" + controller: "ShareProjectModalController" + scope: $scope + ) + + ide.socket.on 'project:membership:changed', (data) => + if data.members + projectMembers.getMembers() + .success (responseData) => + if responseData.members + $scope.project.members = responseData.members + .error (responseDate) => + console.error "Error fetching members for project" + if data.invites + projectInvites.getInvites() + .success (responseData) => + if responseData.invites + $scope.project.invites = responseData.invites + .error (responseDate) => + 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 13d5faea9f..6f95d4e38f 100644 --- a/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee +++ b/services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, $modal, $http) -> + App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http) -> $scope.inputs = { privileges: "readAndWrite" contacts: [] @@ -10,6 +10,7 @@ define [ error: null inflight: false startedFreeTrial: false + invites: [] } $modalInstance.opened.then () -> @@ -18,10 +19,12 @@ define [ , 200 INFINITE_COLLABORATORS = -1 - $scope.$watch "project.members.length", (noOfMembers) -> + $scope.$watch "(project.members.length + project.invites.length)", (noOfMembers) -> allowedNoOfMembers = $scope.project.features.collaborators $scope.canAddCollaborators = noOfMembers < allowedNoOfMembers or allowedNoOfMembers == INFINITE_COLLABORATORS + window._m = projectMembers + $scope.autocompleteContacts = [] do loadAutocompleteUsers = () -> $http.get "/user/contacts" @@ -38,9 +41,12 @@ define [ else # Must be a group contact.display = contact.name - + getCurrentMemberEmails = () -> - $scope.project.members.map (u) -> u.email + ($scope.project.members || []).map (u) -> u.email + + getCurrentInviteEmails = () -> + ($scope.project.invites || []).map (u) -> u.email $scope.filterAutocompleteUsers = ($query) -> currentMemberEmails = getCurrentMemberEmails() @@ -60,36 +66,48 @@ define [ $scope.inputs.contacts = [] $scope.state.error = null $scope.state.inflight = true - + + if !$scope.project.invites? + $scope.project.invites = [] + currentMemberEmails = getCurrentMemberEmails() + currentInviteEmails = getCurrentInviteEmails() do addNextMember = () -> if members.length == 0 or !$scope.canAddCollaborators $scope.state.inflight = false $scope.$apply() return - + member = members.shift() - if !member.type? and member.display in currentMemberEmails + if member.type == "user" + email = member.email + else # Not an auto-complete object, so email == display + email = member.display + email = email.toLowerCase() + + if email in currentMemberEmails # Skip this existing member return addNextMember() - - if member.type == "user" - request = projectMembers.addMember(member.email, $scope.inputs.privileges) - else if member.type == "group" - request = projectMembers.addGroup(member.id, $scope.inputs.privileges) - else # Not an auto-complete object, so email == display - request = projectMembers.addMember(member.display, $scope.inputs.privileges) - + + if email in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == email)?._id + request = projectInvites.resendInvite(inviteId) + else + request = projectInvites.sendInvite(email, $scope.inputs.privileges) + request .success (data) -> - if data.users? - users = data.users - else if data.user? - users = [data.user] + if data.invite + invite = data.invite + $scope.project.invites.push invite else - users = [] - - $scope.project.members.push users... + if data.users? + users = data.users + else if data.user? + users = [data.user] + else + users = [] + $scope.project.members.push users... + setTimeout () -> # Give $scope a chance to update $scope.canAddCollaborators # with new collaborator information. @@ -98,8 +116,7 @@ define [ .error () -> $scope.state.inflight = false $scope.state.error = true - - + $timeout addMembers, 50 # Give email list a chance to update $scope.removeMember = (member) -> @@ -116,6 +133,33 @@ define [ $scope.state.inflight = false $scope.state.error = "Sorry, something went wrong :(" + $scope.revokeInvite = (invite) -> + $scope.state.error = null + $scope.state.inflight = true + projectInvites + .revokeInvite(invite._id) + .success () -> + $scope.state.inflight = false + index = $scope.project.invites.indexOf(invite) + return if index == -1 + $scope.project.invites.splice(index, 1) + .error () -> + $scope.state.inflight = false + $scope.state.error = "Sorry, something went wrong :(" + + $scope.resendInvite = (invite, event) -> + $scope.state.error = null + $scope.state.inflight = true + projectInvites + .resendInvite(invite._id) + .success () -> + $scope.state.inflight = false + event.target.blur() + .error () -> + $scope.state.inflight = false + $scope.state.error = "Sorry, something went wrong resending the invite :(" + event.target.blur() + $scope.openMakePublicModal = () -> $modal.open { templateUrl: "makePublicModalTemplate" @@ -158,4 +202,4 @@ define [ $scope.cancel = () -> $modalInstance.dismiss() - ] \ No newline at end of file + ] diff --git a/services/web/public/coffee/ide/share/index.coffee b/services/web/public/coffee/ide/share/index.coffee index 545a145be3..13b2bdbcfd 100644 --- a/services/web/public/coffee/ide/share/index.coffee +++ b/services/web/public/coffee/ide/share/index.coffee @@ -2,4 +2,5 @@ define [ "ide/share/controllers/ShareController" "ide/share/controllers/ShareProjectModalController" "ide/share/services/projectMembers" -], () -> \ No newline at end of file + "ide/share/services/projectInvites" +], () -> diff --git a/services/web/public/coffee/ide/share/services/projectInvites.coffee b/services/web/public/coffee/ide/share/services/projectInvites.coffee new file mode 100644 index 0000000000..4c0d30add6 --- /dev/null +++ b/services/web/public/coffee/ide/share/services/projectInvites.coffee @@ -0,0 +1,35 @@ +define [ + "base" +], (App) -> + App.factory "projectInvites", ["ide", "$http", (ide, $http) -> + return { + + sendInvite: (email, privileges) -> + $http.post("/project/#{ide.project_id}/invite", { + email: email + privileges: privileges + _csrf: window.csrfToken + }) + + revokeInvite: (inviteId) -> + $http({ + url: "/project/#{ide.project_id}/invite/#{inviteId}" + method: "DELETE" + headers: + "X-Csrf-Token": window.csrfToken + }) + + resendInvite: (inviteId, privileges) -> + $http.post("/project/#{ide.project_id}/invite/#{inviteId}/resend", { + _csrf: window.csrfToken + }) + + getInvites: () -> + $http.get("/project/#{ide.project_id}/invites", { + json: true + headers: + "X-Csrf-Token": window.csrfToken + }) + + } + ] diff --git a/services/web/public/coffee/ide/share/services/projectMembers.coffee b/services/web/public/coffee/ide/share/services/projectMembers.coffee index a51ea63e99..f1b2c8c3fe 100644 --- a/services/web/public/coffee/ide/share/services/projectMembers.coffee +++ b/services/web/public/coffee/ide/share/services/projectMembers.coffee @@ -11,19 +11,19 @@ define [ "X-Csrf-Token": window.csrfToken }) - addMember: (email, privileges) -> - $http.post("/project/#{ide.project_id}/users", { - email: email - privileges: privileges - _csrf: window.csrfToken - }) - addGroup: (group_id, privileges) -> $http.post("/project/#{ide.project_id}/group", { group_id: group_id privileges: privileges _csrf: window.csrfToken }) - + + getMembers: () -> + $http.get("/project/#{ide.project_id}/members", { + json: true + headers: + "X-Csrf-Token": window.csrfToken + }) + } - ] \ No newline at end of file + ] diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee deleted file mode 100644 index 16547a491a..0000000000 --- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee +++ /dev/null @@ -1,34 +0,0 @@ -define [ - "base" -], (App) -> - App.controller "TrackChangesDiffController", ($scope, $modal, ide) -> - $scope.restoreDeletedDoc = () -> - ide.trackChangesManager.restoreDeletedDoc( - $scope.trackChanges.diff.doc - ) - - $scope.openRestoreDiffModal = () -> - $modal.open { - templateUrl: "trackChangesRestoreDiffModalTemplate" - controller: "TrackChangesRestoreDiffModalController" - resolve: - diff: () -> $scope.trackChanges.diff - } - - App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide) -> - $scope.state = - inflight: false - - $scope.diff = diff - - $scope.restore = () -> - $scope.state.inflight = true - ide.trackChangesManager - .restoreDiff(diff) - .success () -> - $scope.state.inflight = false - $modalInstance.close() - ide.editorManager.openDoc(diff.doc) - - $scope.cancel = () -> - $modalInstance.dismiss() diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index d41170391c..307d0307b6 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -1,5 +1,5 @@ define [ - "libs/moment-2.9.0" + "moment" "libs/angular-autocomplete/angular-autocomplete" "libs/ui-bootstrap" "libs/ng-context-menu-0.1.4" diff --git a/services/web/public/coffee/main.coffee b/services/web/public/coffee/main.coffee index d85d89cfe8..60cc38ae6a 100644 --- a/services/web/public/coffee/main.coffee +++ b/services/web/public/coffee/main.coffee @@ -2,6 +2,7 @@ define [ "main/project-list/index" "main/user-details" "main/account-settings" + "main/clear-sessions" "main/account-upgrade" "main/plans" "main/group-members" @@ -26,11 +27,9 @@ define [ "directives/onEnter" "directives/selectAll" "directives/maxHeight" + "directives/creditCards" "services/queued-http" "filters/formatDate" "__MAIN_CLIENTSIDE_INCLUDES__" ], () -> angular.bootstrap(document.body, ["SharelatexApp"]) - - - diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee index 34b92883b0..29ec146051 100644 --- a/services/web/public/coffee/main/account-settings.coffee +++ b/services/web/public/coffee/main/account-settings.coffee @@ -21,7 +21,8 @@ define [ $scope.deleteAccount = () -> modalInstance = $modal.open( templateUrl: "deleteAccountModalTemplate" - controller: "DeleteAccountModalController" + controller: "DeleteAccountModalController", + scope: $scope ) ] @@ -39,7 +40,7 @@ define [ , 700 $scope.checkValidation = -> - $scope.state.isValid = $scope.state.deleteText == "DELETE" + $scope.state.isValid = $scope.state.deleteText == $scope.email $scope.delete = () -> $scope.state.inflight = true @@ -56,4 +57,4 @@ define [ $scope.cancel = () -> $modalInstance.dismiss('cancel') - ] \ No newline at end of file + ] diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 956c340d14..be842b6907 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -6,7 +6,7 @@ define [ $scope.buttonClass = "btn-primary" $scope.startFreeTrial = (source, couponCode) -> - event_tracking.sendCountly "subscription-start-trial", { source } + event_tracking.sendMB "subscription-start-trial", { source } w = window.open() sixpack.convert "track-changes-discount", -> diff --git a/services/web/public/coffee/main/clear-sessions.coffee b/services/web/public/coffee/main/clear-sessions.coffee new file mode 100644 index 0000000000..5524ff8d89 --- /dev/null +++ b/services/web/public/coffee/main/clear-sessions.coffee @@ -0,0 +1,20 @@ +define [ + "base" +], (App) -> + App.controller "ClearSessionsController", ["$scope", "$http", ($scope, $http) -> + + $scope.state = + otherSessions: window.otherSessions + error: false + success: false + + $scope.clearSessions = () -> + console.log ">> clearing all sessions" + $http({method: 'POST', url: "/user/sessions/clear", headers: {'X-CSRF-Token': window.csrfToken}}) + .success () -> + $scope.state.otherSessions = [] + $scope.state.error = false + $scope.state.success = true + .error () -> + $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 0f722df369..2f3a2c61e0 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -24,7 +24,7 @@ define [ url :"/learn/kb/#{page_underscored}" name : hit._highlightResult.pageName.value - event_tracking.sendCountly "contact-form-suggestions-shown" if results.hits.length + event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length $scope.$applyAsync () -> $scope.suggestions = suggestions @@ -60,7 +60,7 @@ define [ $scope.suggestions = []; $scope.clickSuggestionLink = (url) -> - event_tracking.sendCountly "contact-form-suggestions-clicked", { url } + event_tracking.sendMB "contact-form-suggestions-clicked", { url } $scope.close = () -> $modalInstance.close() diff --git a/services/web/public/coffee/main/event.coffee b/services/web/public/coffee/main/event.coffee index 371bd1004e..b2847bc5a0 100644 --- a/services/web/public/coffee/main/event.coffee +++ b/services/web/public/coffee/main/event.coffee @@ -2,9 +2,14 @@ define [ "base" "modules/localStorage" ], (App) -> - CACHE_KEY = "countlyEvents" + CACHE_KEY = "mbEvents" - App.factory "event_tracking", (localStorage) -> + send = (category, action, attributes = {})-> + ga('send', 'event', category, action) + event_name = "#{action}-#{category}" + Intercom?("trackEvent", event_name, attributes) + + App.factory "event_tracking", ($http, localStorage) -> _getEventCache = () -> eventCache = localStorage CACHE_KEY @@ -29,18 +34,23 @@ define [ send: (category, action, label, value)-> ga('send', 'event', category, action, label, value) - sendCountly: (key, segmentation) -> - eventData = { key } - eventData.segmentation = segmentation if segmentation? - Countly?.q.push([ "add_event", eventData ]) + sendMB: (key, segmentation = {}) -> + $http { + url: "/event/#{key}", + method: "POST", + data: segmentation + headers: { + "X-CSRF-Token": window.csrfToken + } + } - sendCountlySampled: (key, segmentation) -> - @sendCountly key, segmentation if Math.random() < .01 + sendMBSampled: (key, segmentation) -> + @sendMB key, segmentation if Math.random() < .01 - sendCountlyOnce: (key, segmentation) -> + sendMBOnce: (key, segmentation) -> if ! _eventInCache(key) _addEventToCache(key) - @sendCountly key, segmentation + @sendMB key, segmentation } # App.directive "countlyTrack", () -> diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index bdae2b219e..d006c6173c 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -1,8 +1,9 @@ define [ - "base" + "base", + "directives/creditCards" ], (App)-> - App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)-> + App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)-> throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined" $scope.currencyCode = MultiCurrencyPricing.currencyCode @@ -11,8 +12,10 @@ define [ $scope.switchToStudent = ()-> window.location = "/user/subscription/new?planCode=student_free_trial_7_days¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}" + event_tracking.sendMB "subscription-form", { plan : window.plan_code } - $scope.paymentMethod = "credit_card" + $scope.paymentMethod = + value: "credit_card" $scope.data = number: "" @@ -28,12 +31,12 @@ define [ city:"" country:window.countryCode coupon: window.couponCode + mmYY: "" - $scope.validation = correctCardNumber : true correctExpiry: true - correctCvv:true + correctCvv: true $scope.processing = false @@ -51,12 +54,11 @@ define [ .done() pricing.on "change", => - event_tracking.sendCountly "subscription-form", { plan : pricing.items.plan.code } - $scope.planName = pricing.items.plan.name $scope.price = pricing.price $scope.trialLength = pricing.items.plan.trial?.length $scope.monthlyBilling = pricing.items.plan.period.length == 1 + if pricing.items?.coupon?.discount?.type == "percent" basePrice = parseInt(pricing.price.base.plan.unit) $scope.normalPrice = basePrice @@ -74,35 +76,58 @@ define [ $scope.applyVatNumber = -> pricing.tax({tax_code: 'digital', vat_number: $scope.data.vat_number}).done() - $scope.changeCurrency = (newCurrency)-> $scope.currencyCode = newCurrency pricing.currency(newCurrency).done() + $scope.updateExpiry = () -> + parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY + if parsedDateObj? + $scope.data.month = parsedDateObj.month + $scope.data.year = parsedDateObj.year + $scope.validateCardNumber = validateCardNumber = -> + $scope.validation.errorFields = {} if $scope.data.number?.length != 0 $scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number) $scope.validateExpiry = validateExpiry = -> + $scope.validation.errorFields = {} if $scope.data.month?.length != 0 and $scope.data.year?.length != 0 $scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year) $scope.validateCvv = validateCvv = -> + $scope.validation.errorFields = {} if $scope.data.cvv?.length != 0 $scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv) + $scope.inputHasError = inputHasError = (formItem) -> + if !formItem? + return false + + return (formItem.$touched && formItem.$invalid) + + $scope.isFormValid = isFormValid = (form) -> + if $scope.paymentMethod.value == 'paypal' + return $scope.data.country != "" + else + return (form.$valid and + $scope.validation.correctCardNumber and + $scope.validation.correctExpiry and + $scope.validation.correctCvv) + $scope.updateCountry = -> pricing.address({country:$scope.data.country}).done() - $scope.changePaymentMethod = (paymentMethod)-> - if paymentMethod == "paypal" - $scope.usePaypal = true - else - $scope.usePaypal = false + $scope.setPaymentMethod = setPaymentMethod = (method) -> + $scope.paymentMethod.value = method; + $scope.validation.errorFields = {} + $scope.genericError = "" completeSubscription = (err, recurly_token_id) -> $scope.validation.errorFields = {} if err? + event_tracking.sendMB "subscription-error", err # We may or may not be in a digest loop here depending on # whether recurly could do validation locally, so do it async $scope.$evalAsync () -> @@ -117,7 +142,7 @@ define [ currencyCode:pricing.items.currency plan_code:pricing.items.plan.code coupon_code:pricing.items?.coupon?.code || "" - isPaypal: $scope.paymentMethod == 'paypal' + isPaypal: $scope.paymentMethod.value == 'paypal' address: address1: $scope.data.address1 address2: $scope.data.address2 @@ -125,25 +150,25 @@ define [ state: $scope.data.state postal_code: $scope.data.postal_code - event_tracking.sendCountly "subscription-form-submitted", { + event_tracking.sendMB "subscription-form-submitted", { currencyCode : postData.subscriptionDetails.currencyCode, plan_code : postData.subscriptionDetails.plan_code, coupon_code : postData.subscriptionDetails.coupon_code, isPaypal : postData.subscriptionDetails.isPaypal } + $http.post("/user/subscription/create", postData) .success (data, status, headers)-> - sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)-> - event_tracking.sendCountly "subscription-submission-success" - window.location.href = "/user/subscription/thank-you" + event_tracking.sendMB "subscription-submission-success" + window.location.href = "/user/subscription/thank-you" .error (data, status, headers)-> $scope.processing = false $scope.genericError = "Something went wrong processing the request" $scope.submit = -> $scope.processing = true - if $scope.paymentMethod == 'paypal' + if $scope.paymentMethod.value == 'paypal' opts = { description: $scope.planName } recurly.paypal opts, completeSubscription else diff --git a/services/web/public/coffee/main/project-list/notifications-controller.coffee b/services/web/public/coffee/main/project-list/notifications-controller.coffee index 36a725f778..c9f2d0c68b 100644 --- a/services/web/public/coffee/main/project-list/notifications-controller.coffee +++ b/services/web/public/coffee/main/project-list/notifications-controller.coffee @@ -15,3 +15,24 @@ define [ }) .success (data) -> notification.hide = true + + App.controller "ProjectInviteNotificationController", ($scope, $http) -> + # Shortcuts for translation keys + $scope.projectName = $scope.notification.messageOpts.projectName + $scope.userName = $scope.notification.messageOpts.userName + + $scope.accept = () -> + $scope.notification.inflight = true + $http({ + url: "/project/#{$scope.notification.messageOpts.projectId}/invite/token/#{$scope.notification.messageOpts.token}/accept" + method: "POST" + headers: + "X-Csrf-Token": window.csrfToken + "X-Requested-With": "XMLHttpRequest" + }) + .success () -> + $scope.notification.inflight = false + $scope.notification.accepted = true + .error () -> + $scope.notification.inflight = false + $scope.notification.error = true \ No newline at end of file diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index a528c0b96f..39ce68043f 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -2,7 +2,7 @@ define [ "base" ], (App) -> - App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout, sixpack) -> + App.controller "ProjectPageController", ($scope, $modal, $q, $window, queuedHttp, event_tracking, $timeout) -> $scope.projects = window.data.projects $scope.tags = window.data.tags $scope.notifications = window.data.notifications @@ -11,15 +11,13 @@ define [ $scope.filter = "all" $scope.predicate = "lastUpdated" $scope.reverse = true + $scope.searchText = + value : "" - if $scope.projects.length > 0 - $scope.first_sign_up = "default" - else - sixpack.participate 'first_sign_up', ['default', 'minimial'], (chosenVariation, rawResponse)-> - $scope.first_sign_up = chosenVariation - $timeout () -> - recalculateProjectListHeight() - , 10 + if $scope.projects.length == 0 + $timeout () -> + recalculateProjectListHeight() + , 10 recalculateProjectListHeight = () -> topOffset = $(".project-list-card")?.offset()?.top @@ -73,7 +71,7 @@ define [ $scope.updateVisibleProjects() $scope.clearSearchText = () -> - $scope.searchText = "" + $scope.searchText.value = "" $scope.filter = "all" $scope.$emit "search:clear" $scope.updateVisibleProjects() @@ -100,8 +98,8 @@ define [ for project in $scope.projects visible = true # Only show if it matches any search text - if $scope.searchText? and $scope.searchText != "" - if !project.name.toLowerCase().match($scope.searchText.toLowerCase()) + if $scope.searchText.value? and $scope.searchText.value != "" + if !project.name.toLowerCase().match($scope.searchText.value.toLowerCase()) visible = false # Only show if it matches the selected tag if $scope.filter == "tag" and selectedTag? and project.id not in selectedTag.project_ids diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee index 63eec0d65a..7476d814e1 100644 --- a/services/web/public/coffee/main/subscription-dashboard.coffee +++ b/services/web/public/coffee/main/subscription-dashboard.coffee @@ -18,7 +18,6 @@ define [ App.controller "ChangePlanFormController", ($scope, $modal, MultiCurrencyPricing)-> setupReturly() - console.log("init") taxRate = window.taxRate $scope.changePlan = -> @@ -62,8 +61,7 @@ define [ $scope.inflight = true - - $http.post(SUBSCRIPTION_URL, body) + $http.post("#{SUBSCRIPTION_URL}?origin=confirmChangePlan", body) .success -> location.reload() .error -> @@ -124,7 +122,7 @@ define [ plan_code: 'student' _csrf : window.csrfToken $scope.inflight = true - $http.post(SUBSCRIPTION_URL, body) + $http.post("#{SUBSCRIPTION_URL}?origin=downgradeToStudent", body) .success -> location.reload() .error -> diff --git a/services/web/public/coffee/utils/EventEmitter.coffee b/services/web/public/coffee/utils/EventEmitter.coffee index 12c34e73f0..b0fe5f1a36 100644 --- a/services/web/public/coffee/utils/EventEmitter.coffee +++ b/services/web/public/coffee/utils/EventEmitter.coffee @@ -30,4 +30,6 @@ define [], () -> trigger: (event, args...) -> @events ||= {} for callback in @events[event] or [] - callback.callback(args...) \ No newline at end of file + callback.callback(args...) + + emit: (args...) -> @trigger(args...) diff --git a/services/web/public/img/crests/harvard.gif b/services/web/public/img/crests/harvard.gif index f9ba6a2298..092e17f4b3 100644 Binary files a/services/web/public/img/crests/harvard.gif and b/services/web/public/img/crests/harvard.gif differ diff --git a/services/web/public/img/teasers/dropbox/teaser-dropbox-editor.png b/services/web/public/img/teasers/dropbox/teaser-dropbox-editor.png new file mode 100644 index 0000000000..164f49ef8c Binary files /dev/null and b/services/web/public/img/teasers/dropbox/teaser-dropbox-editor.png differ diff --git a/services/web/public/img/teasers/dropbox/teaser-dropbox.gif b/services/web/public/img/teasers/dropbox/teaser-dropbox.gif new file mode 100644 index 0000000000..6377d4e0b5 Binary files /dev/null and b/services/web/public/img/teasers/dropbox/teaser-dropbox.gif differ diff --git a/services/web/public/img/teasers/dropbox/teaser-dropbox.mp4 b/services/web/public/img/teasers/dropbox/teaser-dropbox.mp4 new file mode 100644 index 0000000000..f8695f4aa8 Binary files /dev/null and b/services/web/public/img/teasers/dropbox/teaser-dropbox.mp4 differ diff --git a/services/web/public/js/ace-1.2.5/ace.js b/services/web/public/js/ace-1.2.5/ace.js new file mode 100644 index 0000000000..89bef3543d --- /dev/null +++ b/services/web/public/js/ace-1.2.5/ace.js @@ -0,0 +1,19059 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Distributed under the BSD license: + * + * Copyright (c) 2010, Ajax.org B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Ajax.org B.V. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ***** END LICENSE BLOCK ***** */ + +/** + * Define a module along with a payload + * @param module a name for the payload + * @param payload a function to call with (require, exports, module) params + */ + +(function() { + +var ACE_NAMESPACE = "ace"; + +var global = (function() { return this; })(); +if (!global && typeof window != "undefined") global = window; // strict mode + + +if (!ACE_NAMESPACE && typeof requirejs !== "undefined") + return; + + +var define = function(module, deps, payload) { + if (typeof module !== "string") { + if (define.original) + define.original.apply(this, arguments); + else { + console.error("dropping module because define wasn\'t a string."); + console.trace(); + } + return; + } + if (arguments.length == 2) + payload = deps; + if (!define.modules[module]) { + define.payloads[module] = payload; + define.modules[module] = null; + } +}; + +define.modules = {}; +define.payloads = {}; + +/** + * Get at functionality define()ed using the function above + */ +var _require = function(parentId, module, callback) { + if (typeof module === "string") { + var payload = lookup(parentId, module); + if (payload != undefined) { + callback && callback(); + return payload; + } + } else if (Object.prototype.toString.call(module) === "[object Array]") { + var params = []; + for (var i = 0, l = module.length; i < l; ++i) { + var dep = lookup(parentId, module[i]); + if (dep == undefined && require.original) + return; + params.push(dep); + } + return callback && callback.apply(null, params) || true; + } +}; + +var require = function(module, callback) { + var packagedModule = _require("", module, callback); + if (packagedModule == undefined && require.original) + return require.original.apply(this, arguments); + return packagedModule; +}; + +var normalizeModule = function(parentId, moduleName) { + // normalize plugin requires + if (moduleName.indexOf("!") !== -1) { + var chunks = moduleName.split("!"); + return normalizeModule(parentId, chunks[0]) + "!" + normalizeModule(parentId, chunks[1]); + } + // normalize relative requires + if (moduleName.charAt(0) == ".") { + var base = parentId.split("/").slice(0, -1).join("/"); + moduleName = base + "/" + moduleName; + + while(moduleName.indexOf(".") !== -1 && previous != moduleName) { + var previous = moduleName; + moduleName = moduleName.replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, ""); + } + } + return moduleName; +}; + +/** + * Internal function to lookup moduleNames and resolve them by calling the + * definition function if needed. + */ +var lookup = function(parentId, moduleName) { + moduleName = normalizeModule(parentId, moduleName); + + var module = define.modules[moduleName]; + if (!module) { + module = define.payloads[moduleName]; + if (typeof module === 'function') { + var exports = {}; + var mod = { + id: moduleName, + uri: '', + exports: exports, + packaged: true + }; + + var req = function(module, callback) { + return _require(moduleName, module, callback); + }; + + var returnValue = module(req, exports, mod); + exports = returnValue || mod.exports; + define.modules[moduleName] = exports; + delete define.payloads[moduleName]; + } + module = define.modules[moduleName] = exports || module; + } + return module; +}; + +function exportAce(ns) { + var root = global; + if (ns) { + if (!global[ns]) + global[ns] = {}; + root = global[ns]; + } + + if (!root.define || !root.define.packaged) { + define.original = root.define; + root.define = define; + root.define.packaged = true; + } + + if (!root.require || !root.require.packaged) { + require.original = root.require; + root.require = require; + root.require.packaged = true; + } +} + +exportAce(ACE_NAMESPACE); + +})(); + +ace.define("ace/lib/regexp",["require","exports","module"], function(require, exports, module) { +"use strict"; + + var real = { + exec: RegExp.prototype.exec, + test: RegExp.prototype.test, + match: String.prototype.match, + replace: String.prototype.replace, + split: String.prototype.split + }, + compliantExecNpcg = real.exec.call(/()??/, "")[1] === undefined, // check `exec` handling of nonparticipating capturing groups + compliantLastIndexIncrement = function () { + var x = /^/g; + real.test.call(x, ""); + return !x.lastIndex; + }(); + + if (compliantLastIndexIncrement && compliantExecNpcg) + return; + RegExp.prototype.exec = function (str) { + var match = real.exec.apply(this, arguments), + name, r2; + if ( typeof(str) == 'string' && match) { + if (!compliantExecNpcg && match.length > 1 && indexOf(match, "") > -1) { + r2 = RegExp(this.source, real.replace.call(getNativeFlags(this), "g", "")); + real.replace.call(str.slice(match.index), r2, function () { + for (var i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undefined) + match[i] = undefined; + } + }); + } + if (this._xregexp && this._xregexp.captureNames) { + for (var i = 1; i < match.length; i++) { + name = this._xregexp.captureNames[i - 1]; + if (name) + match[name] = match[i]; + } + } + if (!compliantLastIndexIncrement && this.global && !match[0].length && (this.lastIndex > match.index)) + this.lastIndex--; + } + return match; + }; + if (!compliantLastIndexIncrement) { + RegExp.prototype.test = function (str) { + var match = real.exec.call(this, str); + if (match && this.global && !match[0].length && (this.lastIndex > match.index)) + this.lastIndex--; + return !!match; + }; + } + + function getNativeFlags (regex) { + return (regex.global ? "g" : "") + + (regex.ignoreCase ? "i" : "") + + (regex.multiline ? "m" : "") + + (regex.extended ? "x" : "") + // Proposed for ES4; included in AS3 + (regex.sticky ? "y" : ""); + } + + function indexOf (array, item, from) { + if (Array.prototype.indexOf) // Use the native array method if available + return array.indexOf(item, from); + for (var i = from || 0; i < array.length; i++) { + if (array[i] === item) + return i; + } + return -1; + } + +}); + +ace.define("ace/lib/es5-shim",["require","exports","module"], function(require, exports, module) { + +function Empty() {} + +if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { // .length is 1 + var target = this; + if (typeof target != "function") { + throw new TypeError("Function.prototype.bind called on incompatible " + target); + } + var args = slice.call(arguments, 1); // for normal call + var bound = function () { + + if (this instanceof bound) { + + var result = target.apply( + this, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return this; + + } else { + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + if(target.prototype) { + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + return bound; + }; +} +var call = Function.prototype.call; +var prototypeOfArray = Array.prototype; +var prototypeOfObject = Object.prototype; +var slice = prototypeOfArray.slice; +var _toString = call.bind(prototypeOfObject.toString); +var owns = call.bind(prototypeOfObject.hasOwnProperty); +var defineGetter; +var defineSetter; +var lookupGetter; +var lookupSetter; +var supportsAccessors; +if ((supportsAccessors = owns(prototypeOfObject, "__defineGetter__"))) { + defineGetter = call.bind(prototypeOfObject.__defineGetter__); + defineSetter = call.bind(prototypeOfObject.__defineSetter__); + lookupGetter = call.bind(prototypeOfObject.__lookupGetter__); + lookupSetter = call.bind(prototypeOfObject.__lookupSetter__); +} +if ([1,2].splice(0).length != 2) { + if(function() { // test IE < 9 to splice bug - see issue #138 + function makeArray(l) { + var a = new Array(l+2); + a[0] = a[1] = 0; + return a; + } + var array = [], lengthBefore; + + array.splice.apply(array, makeArray(20)); + array.splice.apply(array, makeArray(26)); + + lengthBefore = array.length; //46 + array.splice(5, 0, "XXX"); // add one element + + lengthBefore + 1 == array.length + + if (lengthBefore + 1 == array.length) { + return true;// has right splice implementation without bugs + } + }()) {//IE 6/7 + var array_splice = Array.prototype.splice; + Array.prototype.splice = function(start, deleteCount) { + if (!arguments.length) { + return []; + } else { + return array_splice.apply(this, [ + start === void 0 ? 0 : start, + deleteCount === void 0 ? (this.length - start) : deleteCount + ].concat(slice.call(arguments, 2))) + } + }; + } else {//IE8 + Array.prototype.splice = function(pos, removeCount){ + var length = this.length; + if (pos > 0) { + if (pos > length) + pos = length; + } else if (pos == void 0) { + pos = 0; + } else if (pos < 0) { + pos = Math.max(length + pos, 0); + } + + if (!(pos+removeCount < length)) + removeCount = length - pos; + + var removed = this.slice(pos, pos+removeCount); + var insert = slice.call(arguments, 2); + var add = insert.length; + if (pos === length) { + if (add) { + this.push.apply(this, insert); + } + } else { + var remove = Math.min(removeCount, length - pos); + var tailOldPos = pos + remove; + var tailNewPos = tailOldPos + add - remove; + var tailCount = length - tailOldPos; + var lengthAfterRemove = length - remove; + + if (tailNewPos < tailOldPos) { // case A + for (var i = 0; i < tailCount; ++i) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } else if (tailNewPos > tailOldPos) { // case B + for (i = tailCount; i--; ) { + this[tailNewPos+i] = this[tailOldPos+i]; + } + } // else, add == remove (nothing to do) + + if (add && pos === lengthAfterRemove) { + this.length = lengthAfterRemove; // truncate array + this.push.apply(this, insert); + } else { + this.length = lengthAfterRemove + add; // reserves space + for (i = 0; i < add; ++i) { + this[pos+i] = insert[i]; + } + } + } + return removed; + }; + } +} +if (!Array.isArray) { + Array.isArray = function isArray(obj) { + return _toString(obj) == "[object Array]"; + }; +} +var boxedString = Object("a"), + splitString = boxedString[0] != "a" || !(0 in boxedString); + +if (!Array.prototype.forEach) { + Array.prototype.forEach = function forEach(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + thisp = arguments[1], + i = -1, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(); // TODO message + } + + while (++i < length) { + if (i in self) { + fun.call(thisp, self[i], i, object); + } + } + }; +} +if (!Array.prototype.map) { + Array.prototype.map = function map(fun /*, thisp*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = Array(length), + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) + result[i] = fun.call(thisp, self[i], i, object); + } + return result; + }; +} +if (!Array.prototype.filter) { + Array.prototype.filter = function filter(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + result = [], + value, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self) { + value = self[i]; + if (fun.call(thisp, value, i, object)) { + result.push(value); + } + } + } + return result; + }; +} +if (!Array.prototype.every) { + Array.prototype.every = function every(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && !fun.call(thisp, self[i], i, object)) { + return false; + } + } + return true; + }; +} +if (!Array.prototype.some) { + Array.prototype.some = function some(fun /*, thisp */) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0, + thisp = arguments[1]; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + + for (var i = 0; i < length; i++) { + if (i in self && fun.call(thisp, self[i], i, object)) { + return true; + } + } + return false; + }; +} +if (!Array.prototype.reduce) { + Array.prototype.reduce = function reduce(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduce of empty array with no initial value"); + } + + var i = 0; + var result; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i++]; + break; + } + if (++i >= length) { + throw new TypeError("reduce of empty array with no initial value"); + } + } while (true); + } + + for (; i < length; i++) { + if (i in self) { + result = fun.call(void 0, result, self[i], i, object); + } + } + + return result; + }; +} +if (!Array.prototype.reduceRight) { + Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) { + var object = toObject(this), + self = splitString && _toString(this) == "[object String]" ? + this.split("") : + object, + length = self.length >>> 0; + if (_toString(fun) != "[object Function]") { + throw new TypeError(fun + " is not a function"); + } + if (!length && arguments.length == 1) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + + var result, i = length - 1; + if (arguments.length >= 2) { + result = arguments[1]; + } else { + do { + if (i in self) { + result = self[i--]; + break; + } + if (--i < 0) { + throw new TypeError("reduceRight of empty array with no initial value"); + } + } while (true); + } + + do { + if (i in this) { + result = fun.call(void 0, result, self[i], i, object); + } + } while (i--); + + return result; + }; +} +if (!Array.prototype.indexOf || ([0, 1].indexOf(1, 2) != -1)) { + Array.prototype.indexOf = function indexOf(sought /*, fromIndex */ ) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + + var i = 0; + if (arguments.length > 1) { + i = toInteger(arguments[1]); + } + i = i >= 0 ? i : Math.max(0, length + i); + for (; i < length; i++) { + if (i in self && self[i] === sought) { + return i; + } + } + return -1; + }; +} +if (!Array.prototype.lastIndexOf || ([0, 1].lastIndexOf(0, -3) != -1)) { + Array.prototype.lastIndexOf = function lastIndexOf(sought /*, fromIndex */) { + var self = splitString && _toString(this) == "[object String]" ? + this.split("") : + toObject(this), + length = self.length >>> 0; + + if (!length) { + return -1; + } + var i = length - 1; + if (arguments.length > 1) { + i = Math.min(i, toInteger(arguments[1])); + } + i = i >= 0 ? i : length - Math.abs(i); + for (; i >= 0; i--) { + if (i in self && sought === self[i]) { + return i; + } + } + return -1; + }; +} +if (!Object.getPrototypeOf) { + Object.getPrototypeOf = function getPrototypeOf(object) { + return object.__proto__ || ( + object.constructor ? + object.constructor.prototype : + prototypeOfObject + ); + }; +} +if (!Object.getOwnPropertyDescriptor) { + var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " + + "non-object: "; + Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT + object); + if (!owns(object, property)) + return; + + var descriptor, getter, setter; + descriptor = { enumerable: true, configurable: true }; + if (supportsAccessors) { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + + var getter = lookupGetter(object, property); + var setter = lookupSetter(object, property); + object.__proto__ = prototype; + + if (getter || setter) { + if (getter) descriptor.get = getter; + if (setter) descriptor.set = setter; + return descriptor; + } + } + descriptor.value = object[property]; + return descriptor; + }; +} +if (!Object.getOwnPropertyNames) { + Object.getOwnPropertyNames = function getOwnPropertyNames(object) { + return Object.keys(object); + }; +} +if (!Object.create) { + var createEmpty; + if (Object.prototype.__proto__ === null) { + createEmpty = function () { + return { "__proto__": null }; + }; + } else { + createEmpty = function () { + var empty = {}; + for (var i in empty) + empty[i] = null; + empty.constructor = + empty.hasOwnProperty = + empty.propertyIsEnumerable = + empty.isPrototypeOf = + empty.toLocaleString = + empty.toString = + empty.valueOf = + empty.__proto__ = null; + return empty; + } + } + + Object.create = function create(prototype, properties) { + var object; + if (prototype === null) { + object = createEmpty(); + } else { + if (typeof prototype != "object") + throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'"); + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (properties !== void 0) + Object.defineProperties(object, properties); + return object; + }; +} + +function doesDefinePropertyWork(object) { + try { + Object.defineProperty(object, "sentinel", {}); + return "sentinel" in object; + } catch (exception) { + } +} +if (Object.defineProperty) { + var definePropertyWorksOnObject = doesDefinePropertyWork({}); + var definePropertyWorksOnDom = typeof document == "undefined" || + doesDefinePropertyWork(document.createElement("div")); + if (!definePropertyWorksOnObject || !definePropertyWorksOnDom) { + var definePropertyFallback = Object.defineProperty; + } +} + +if (!Object.defineProperty || definePropertyFallback) { + var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: "; + var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: " + var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " + + "on this javascript engine"; + + Object.defineProperty = function defineProperty(object, property, descriptor) { + if ((typeof object != "object" && typeof object != "function") || object === null) + throw new TypeError(ERR_NON_OBJECT_TARGET + object); + if ((typeof descriptor != "object" && typeof descriptor != "function") || descriptor === null) + throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor); + if (definePropertyFallback) { + try { + return definePropertyFallback.call(Object, object, property, descriptor); + } catch (exception) { + } + } + if (owns(descriptor, "value")) { + + if (supportsAccessors && (lookupGetter(object, property) || + lookupSetter(object, property))) + { + var prototype = object.__proto__; + object.__proto__ = prototypeOfObject; + delete object[property]; + object[property] = descriptor.value; + object.__proto__ = prototype; + } else { + object[property] = descriptor.value; + } + } else { + if (!supportsAccessors) + throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); + if (owns(descriptor, "get")) + defineGetter(object, property, descriptor.get); + if (owns(descriptor, "set")) + defineSetter(object, property, descriptor.set); + } + + return object; + }; +} +if (!Object.defineProperties) { + Object.defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (owns(properties, property)) + Object.defineProperty(object, property, properties[property]); + } + return object; + }; +} +if (!Object.seal) { + Object.seal = function seal(object) { + return object; + }; +} +if (!Object.freeze) { + Object.freeze = function freeze(object) { + return object; + }; +} +try { + Object.freeze(function () {}); +} catch (exception) { + Object.freeze = (function freeze(freezeObject) { + return function freeze(object) { + if (typeof object == "function") { + return object; + } else { + return freezeObject(object); + } + }; + })(Object.freeze); +} +if (!Object.preventExtensions) { + Object.preventExtensions = function preventExtensions(object) { + return object; + }; +} +if (!Object.isSealed) { + Object.isSealed = function isSealed(object) { + return false; + }; +} +if (!Object.isFrozen) { + Object.isFrozen = function isFrozen(object) { + return false; + }; +} +if (!Object.isExtensible) { + Object.isExtensible = function isExtensible(object) { + if (Object(object) === object) { + throw new TypeError(); // TODO message + } + var name = ''; + while (owns(object, name)) { + name += '?'; + } + object[name] = true; + var returnValue = owns(object, name); + delete object[name]; + return returnValue; + }; +} +if (!Object.keys) { + var hasDontEnumBug = true, + dontEnums = [ + "toString", + "toLocaleString", + "valueOf", + "hasOwnProperty", + "isPrototypeOf", + "propertyIsEnumerable", + "constructor" + ], + dontEnumsLength = dontEnums.length; + + for (var key in {"toString": null}) { + hasDontEnumBug = false; + } + + Object.keys = function keys(object) { + + if ( + (typeof object != "object" && typeof object != "function") || + object === null + ) { + throw new TypeError("Object.keys called on a non-object"); + } + + var keys = []; + for (var name in object) { + if (owns(object, name)) { + keys.push(name); + } + } + + if (hasDontEnumBug) { + for (var i = 0, ii = dontEnumsLength; i < ii; i++) { + var dontEnum = dontEnums[i]; + if (owns(object, dontEnum)) { + keys.push(dontEnum); + } + } + } + return keys; + }; + +} +if (!Date.now) { + Date.now = function now() { + return new Date().getTime(); + }; +} +var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; +if (!String.prototype.trim || ws.trim()) { + ws = "[" + ws + "]"; + var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), + trimEndRegexp = new RegExp(ws + ws + "*$"); + String.prototype.trim = function trim() { + return String(this).replace(trimBeginRegexp, "").replace(trimEndRegexp, ""); + }; +} + +function toInteger(n) { + n = +n; + if (n !== n) { // isNaN + n = 0; + } else if (n !== 0 && n !== (1/0) && n !== -(1/0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + return n; +} + +function isPrimitive(input) { + var type = typeof input; + return ( + input === null || + type === "undefined" || + type === "boolean" || + type === "number" || + type === "string" + ); +} + +function toPrimitive(input) { + var val, valueOf, toString; + if (isPrimitive(input)) { + return input; + } + valueOf = input.valueOf; + if (typeof valueOf === "function") { + val = valueOf.call(input); + if (isPrimitive(val)) { + return val; + } + } + toString = input.toString; + if (typeof toString === "function") { + val = toString.call(input); + if (isPrimitive(val)) { + return val; + } + } + throw new TypeError(); +} +var toObject = function (o) { + if (o == null) { // this matches both null and undefined + throw new TypeError("can't convert "+o+" to object"); + } + return Object(o); +}; + +}); + +ace.define("ace/lib/fixoldbrowsers",["require","exports","module","ace/lib/regexp","ace/lib/es5-shim"], function(require, exports, module) { +"use strict"; + +require("./regexp"); +require("./es5-shim"); + +}); + +ace.define("ace/lib/dom",["require","exports","module"], function(require, exports, module) { +"use strict"; + +var XHTML_NS = "http://www.w3.org/1999/xhtml"; + +exports.getDocumentHead = function(doc) { + if (!doc) + doc = document; + return doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement; +} + +exports.createElement = function(tag, ns) { + return document.createElementNS ? + document.createElementNS(ns || XHTML_NS, tag) : + document.createElement(tag); +}; + +exports.hasCssClass = function(el, name) { + var classes = (el.className + "").split(/\s+/g); + return classes.indexOf(name) !== -1; +}; +exports.addCssClass = function(el, name) { + if (!exports.hasCssClass(el, name)) { + el.className += " " + name; + } +}; +exports.removeCssClass = function(el, name) { + var classes = el.className.split(/\s+/g); + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + classes.splice(index, 1); + } + el.className = classes.join(" "); +}; + +exports.toggleCssClass = function(el, name) { + var classes = el.className.split(/\s+/g), add = true; + while (true) { + var index = classes.indexOf(name); + if (index == -1) { + break; + } + add = false; + classes.splice(index, 1); + } + if (add) + classes.push(name); + + el.className = classes.join(" "); + return add; +}; +exports.setCssClass = function(node, className, include) { + if (include) { + exports.addCssClass(node, className); + } else { + exports.removeCssClass(node, className); + } +}; + +exports.hasCssString = function(id, doc) { + var index = 0, sheets; + doc = doc || document; + + if (doc.createStyleSheet && (sheets = doc.styleSheets)) { + while (index < sheets.length) + if (sheets[index++].owningElement.id === id) return true; + } else if ((sheets = doc.getElementsByTagName("style"))) { + while (index < sheets.length) + if (sheets[index++].id === id) return true; + } + + return false; +}; + +exports.importCssString = function importCssString(cssText, id, doc) { + doc = doc || document; + if (id && exports.hasCssString(id, doc)) + return null; + + var style; + + if (id) + cssText += "\n/*# sourceURL=ace/css/" + id + " */"; + + if (doc.createStyleSheet) { + style = doc.createStyleSheet(); + style.cssText = cssText; + if (id) + style.owningElement.id = id; + } else { + style = exports.createElement("style"); + style.appendChild(doc.createTextNode(cssText)); + if (id) + style.id = id; + + exports.getDocumentHead(doc).appendChild(style); + } +}; + +exports.importCssStylsheet = function(uri, doc) { + if (doc.createStyleSheet) { + doc.createStyleSheet(uri); + } else { + var link = exports.createElement('link'); + link.rel = 'stylesheet'; + link.href = uri; + + exports.getDocumentHead(doc).appendChild(link); + } +}; + +exports.getInnerWidth = function(element) { + return ( + parseInt(exports.computedStyle(element, "paddingLeft"), 10) + + parseInt(exports.computedStyle(element, "paddingRight"), 10) + + element.clientWidth + ); +}; + +exports.getInnerHeight = function(element) { + return ( + parseInt(exports.computedStyle(element, "paddingTop"), 10) + + parseInt(exports.computedStyle(element, "paddingBottom"), 10) + + element.clientHeight + ); +}; + +exports.scrollbarWidth = function(document) { + var inner = exports.createElement("ace_inner"); + inner.style.width = "100%"; + inner.style.minWidth = "0px"; + inner.style.height = "200px"; + inner.style.display = "block"; + + var outer = exports.createElement("ace_outer"); + var style = outer.style; + + style.position = "absolute"; + style.left = "-10000px"; + style.overflow = "hidden"; + style.width = "200px"; + style.minWidth = "0px"; + style.height = "150px"; + style.display = "block"; + + outer.appendChild(inner); + + var body = document.documentElement; + body.appendChild(outer); + + var noScrollbar = inner.offsetWidth; + + style.overflow = "scroll"; + var withScrollbar = inner.offsetWidth; + + if (noScrollbar == withScrollbar) { + withScrollbar = outer.clientWidth; + } + + body.removeChild(outer); + + return noScrollbar-withScrollbar; +}; + +if (typeof document == "undefined") { + exports.importCssString = function() {}; + return; +} + +if (window.pageYOffset !== undefined) { + exports.getPageScrollTop = function() { + return window.pageYOffset; + }; + + exports.getPageScrollLeft = function() { + return window.pageXOffset; + }; +} +else { + exports.getPageScrollTop = function() { + return document.body.scrollTop; + }; + + exports.getPageScrollLeft = function() { + return document.body.scrollLeft; + }; +} + +if (window.getComputedStyle) + exports.computedStyle = function(element, style) { + if (style) + return (window.getComputedStyle(element, "") || {})[style] || ""; + return window.getComputedStyle(element, "") || {}; + }; +else + exports.computedStyle = function(element, style) { + if (style) + return element.currentStyle[style]; + return element.currentStyle; + }; +exports.setInnerHtml = function(el, innerHtml) { + var element = el.cloneNode(false);//document.createElement("div"); + element.innerHTML = innerHtml; + el.parentNode.replaceChild(element, el); + return element; +}; + +if ("textContent" in document.documentElement) { + exports.setInnerText = function(el, innerText) { + el.textContent = innerText; + }; + + exports.getInnerText = function(el) { + return el.textContent; + }; +} +else { + exports.setInnerText = function(el, innerText) { + el.innerText = innerText; + }; + + exports.getInnerText = function(el) { + return el.innerText; + }; +} + +exports.getParentWindow = function(document) { + return document.defaultView || document.parentWindow; +}; + +}); + +ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports.mixin = function(obj, mixin) { + for (var key in mixin) { + obj[key] = mixin[key]; + } + return obj; +}; + +exports.implement = function(proto, mixin) { + exports.mixin(proto, mixin); +}; + +}); + +ace.define("ace/lib/keys",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/oop"], function(require, exports, module) { +"use strict"; + +require("./fixoldbrowsers"); + +var oop = require("./oop"); +var Keys = (function() { + var ret = { + MODIFIER_KEYS: { + 16: 'Shift', 17: 'Ctrl', 18: 'Alt', 224: 'Meta' + }, + + KEY_MODS: { + "ctrl": 1, "alt": 2, "option" : 2, "shift": 4, + "super": 8, "meta": 8, "command": 8, "cmd": 8 + }, + + FUNCTION_KEYS : { + 8 : "Backspace", + 9 : "Tab", + 13 : "Return", + 19 : "Pause", + 27 : "Esc", + 32 : "Space", + 33 : "PageUp", + 34 : "PageDown", + 35 : "End", + 36 : "Home", + 37 : "Left", + 38 : "Up", + 39 : "Right", + 40 : "Down", + 44 : "Print", + 45 : "Insert", + 46 : "Delete", + 96 : "Numpad0", + 97 : "Numpad1", + 98 : "Numpad2", + 99 : "Numpad3", + 100: "Numpad4", + 101: "Numpad5", + 102: "Numpad6", + 103: "Numpad7", + 104: "Numpad8", + 105: "Numpad9", + '-13': "NumpadEnter", + 112: "F1", + 113: "F2", + 114: "F3", + 115: "F4", + 116: "F5", + 117: "F6", + 118: "F7", + 119: "F8", + 120: "F9", + 121: "F10", + 122: "F11", + 123: "F12", + 144: "Numlock", + 145: "Scrolllock" + }, + + PRINTABLE_KEYS: { + 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', + 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a', + 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', + 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', + 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', + 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.', + 186: ';', 187: '=', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`', + 219: '[', 220: '\\',221: ']', 222: "'", 111: '/', 106: '*' + } + }; + var name, i; + for (i in ret.FUNCTION_KEYS) { + name = ret.FUNCTION_KEYS[i].toLowerCase(); + ret[name] = parseInt(i, 10); + } + for (i in ret.PRINTABLE_KEYS) { + name = ret.PRINTABLE_KEYS[i].toLowerCase(); + ret[name] = parseInt(i, 10); + } + oop.mixin(ret, ret.MODIFIER_KEYS); + oop.mixin(ret, ret.PRINTABLE_KEYS); + oop.mixin(ret, ret.FUNCTION_KEYS); + ret.enter = ret["return"]; + ret.escape = ret.esc; + ret.del = ret["delete"]; + ret[173] = '-'; + + (function() { + var mods = ["cmd", "ctrl", "alt", "shift"]; + for (var i = Math.pow(2, mods.length); i--;) { + ret.KEY_MODS[i] = mods.filter(function(x) { + return i & ret.KEY_MODS[x]; + }).join("-") + "-"; + } + })(); + + ret.KEY_MODS[0] = ""; + ret.KEY_MODS[-1] = "input-"; + + return ret; +})(); +oop.mixin(exports, Keys); + +exports.keyCodeToString = function(keyCode) { + var keyString = Keys[keyCode]; + if (typeof keyString != "string") + keyString = String.fromCharCode(keyCode); + return keyString.toLowerCase(); +}; + +}); + +ace.define("ace/lib/useragent",["require","exports","module"], function(require, exports, module) { +"use strict"; +exports.OS = { + LINUX: "LINUX", + MAC: "MAC", + WINDOWS: "WINDOWS" +}; +exports.getOS = function() { + if (exports.isMac) { + return exports.OS.MAC; + } else if (exports.isLinux) { + return exports.OS.LINUX; + } else { + return exports.OS.WINDOWS; + } +}; +if (typeof navigator != "object") + return; + +var os = (navigator.platform.match(/mac|win|linux/i) || ["other"])[0].toLowerCase(); +var ua = navigator.userAgent; +exports.isWin = (os == "win"); +exports.isMac = (os == "mac"); +exports.isLinux = (os == "linux"); +exports.isIE = + (navigator.appName == "Microsoft Internet Explorer" || navigator.appName.indexOf("MSAppHost") >= 0) + ? parseFloat((ua.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]) + : parseFloat((ua.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]); // for ie + +exports.isOldIE = exports.isIE && exports.isIE < 9; +exports.isGecko = exports.isMozilla = (window.Controllers || window.controllers) && window.navigator.product === "Gecko"; +exports.isOldGecko = exports.isGecko && parseInt((ua.match(/rv:(\d+)/)||[])[1], 10) < 4; +exports.isOpera = window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]"; +exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined; + +exports.isChrome = parseFloat(ua.split(" Chrome/")[1]) || undefined; + +exports.isAIR = ua.indexOf("AdobeAIR") >= 0; + +exports.isIPad = ua.indexOf("iPad") >= 0; + +exports.isTouchPad = ua.indexOf("TouchPad") >= 0; + +exports.isChromeOS = ua.indexOf(" CrOS ") >= 0; + +}); + +ace.define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"], function(require, exports, module) { +"use strict"; + +var keys = require("./keys"); +var useragent = require("./useragent"); + +var pressedKeys = null; +var ts = 0; + +exports.addListener = function(elem, type, callback) { + if (elem.addEventListener) { + return elem.addEventListener(type, callback, false); + } + if (elem.attachEvent) { + var wrapper = function() { + callback.call(elem, window.event); + }; + callback._wrapper = wrapper; + elem.attachEvent("on" + type, wrapper); + } +}; + +exports.removeListener = function(elem, type, callback) { + if (elem.removeEventListener) { + return elem.removeEventListener(type, callback, false); + } + if (elem.detachEvent) { + elem.detachEvent("on" + type, callback._wrapper || callback); + } +}; +exports.stopEvent = function(e) { + exports.stopPropagation(e); + exports.preventDefault(e); + return false; +}; + +exports.stopPropagation = function(e) { + if (e.stopPropagation) + e.stopPropagation(); + else + e.cancelBubble = true; +}; + +exports.preventDefault = function(e) { + if (e.preventDefault) + e.preventDefault(); + else + e.returnValue = false; +}; +exports.getButton = function(e) { + if (e.type == "dblclick") + return 0; + if (e.type == "contextmenu" || (useragent.isMac && (e.ctrlKey && !e.altKey && !e.shiftKey))) + return 2; + if (e.preventDefault) { + return e.button; + } + else { + return {1:0, 2:2, 4:1}[e.button]; + } +}; + +exports.capture = function(el, eventHandler, releaseCaptureHandler) { + function onMouseUp(e) { + eventHandler && eventHandler(e); + releaseCaptureHandler && releaseCaptureHandler(e); + + exports.removeListener(document, "mousemove", eventHandler, true); + exports.removeListener(document, "mouseup", onMouseUp, true); + exports.removeListener(document, "dragstart", onMouseUp, true); + } + + exports.addListener(document, "mousemove", eventHandler, true); + exports.addListener(document, "mouseup", onMouseUp, true); + exports.addListener(document, "dragstart", onMouseUp, true); + + return onMouseUp; +}; + +exports.addTouchMoveListener = function (el, callback) { + if ("ontouchmove" in el) { + var startx, starty; + exports.addListener(el, "touchstart", function (e) { + var touchObj = e.changedTouches[0]; + startx = touchObj.clientX; + starty = touchObj.clientY; + }); + exports.addListener(el, "touchmove", function (e) { + var factor = 1, + touchObj = e.changedTouches[0]; + + e.wheelX = -(touchObj.clientX - startx) / factor; + e.wheelY = -(touchObj.clientY - starty) / factor; + + startx = touchObj.clientX; + starty = touchObj.clientY; + + callback(e); + }); + } +}; + +exports.addMouseWheelListener = function(el, callback) { + if ("onmousewheel" in el) { + exports.addListener(el, "mousewheel", function(e) { + var factor = 8; + if (e.wheelDeltaX !== undefined) { + e.wheelX = -e.wheelDeltaX / factor; + e.wheelY = -e.wheelDeltaY / factor; + } else { + e.wheelX = 0; + e.wheelY = -e.wheelDelta / factor; + } + callback(e); + }); + } else if ("onwheel" in el) { + exports.addListener(el, "wheel", function(e) { + var factor = 0.35; + switch (e.deltaMode) { + case e.DOM_DELTA_PIXEL: + e.wheelX = e.deltaX * factor || 0; + e.wheelY = e.deltaY * factor || 0; + break; + case e.DOM_DELTA_LINE: + case e.DOM_DELTA_PAGE: + e.wheelX = (e.deltaX || 0) * 5; + e.wheelY = (e.deltaY || 0) * 5; + break; + } + + callback(e); + }); + } else { + exports.addListener(el, "DOMMouseScroll", function(e) { + if (e.axis && e.axis == e.HORIZONTAL_AXIS) { + e.wheelX = (e.detail || 0) * 5; + e.wheelY = 0; + } else { + e.wheelX = 0; + e.wheelY = (e.detail || 0) * 5; + } + callback(e); + }); + } +}; + +exports.addMultiMouseDownListener = function(elements, timeouts, eventHandler, callbackName) { + var clicks = 0; + var startX, startY, timer; + var eventNames = { + 2: "dblclick", + 3: "tripleclick", + 4: "quadclick" + }; + + function onMousedown(e) { + if (exports.getButton(e) !== 0) { + clicks = 0; + } else if (e.detail > 1) { + clicks++; + if (clicks > 4) + clicks = 1; + } else { + clicks = 1; + } + if (useragent.isIE) { + var isNewClick = Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5; + if (!timer || isNewClick) + clicks = 1; + if (timer) + clearTimeout(timer); + timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600); + + if (clicks == 1) { + startX = e.clientX; + startY = e.clientY; + } + } + + e._clicks = clicks; + + eventHandler[callbackName]("mousedown", e); + + if (clicks > 4) + clicks = 0; + else if (clicks > 1) + return eventHandler[callbackName](eventNames[clicks], e); + } + function onDblclick(e) { + clicks = 2; + if (timer) + clearTimeout(timer); + timer = setTimeout(function() {timer = null}, timeouts[clicks - 1] || 600); + eventHandler[callbackName]("mousedown", e); + eventHandler[callbackName](eventNames[clicks], e); + } + if (!Array.isArray(elements)) + elements = [elements]; + elements.forEach(function(el) { + exports.addListener(el, "mousedown", onMousedown); + if (useragent.isOldIE) + exports.addListener(el, "dblclick", onDblclick); + }); +}; + +var getModifierHash = useragent.isMac && useragent.isOpera && !("KeyboardEvent" in window) + ? function(e) { + return 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0); + } + : function(e) { + return 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0); + }; + +exports.getModifierString = function(e) { + return keys.KEY_MODS[getModifierHash(e)]; +}; + +function normalizeCommandKeys(callback, e, keyCode) { + var hashId = getModifierHash(e); + + if (!useragent.isMac && pressedKeys) { + if (e.getModifierState && (e.getModifierState("OS") || e.getModifierState("Win"))) + hashId |= 8; + if (pressedKeys.altGr) { + if ((3 & hashId) != 3) + pressedKeys.altGr = 0; + else + return; + } + if (keyCode === 18 || keyCode === 17) { + var location = "location" in e ? e.location : e.keyLocation; + if (keyCode === 17 && location === 1) { + if (pressedKeys[keyCode] == 1) + ts = e.timeStamp; + } else if (keyCode === 18 && hashId === 3 && location === 2) { + var dt = e.timeStamp - ts; + if (dt < 50) + pressedKeys.altGr = true; + } + } + } + + if (keyCode in keys.MODIFIER_KEYS) { + keyCode = -1; + } + if (hashId & 8 && (keyCode >= 91 && keyCode <= 93)) { + keyCode = -1; + } + + if (!hashId && keyCode === 13) { + var location = "location" in e ? e.location : e.keyLocation; + if (location === 3) { + callback(e, hashId, -keyCode); + if (e.defaultPrevented) + return; + } + } + + if (useragent.isChromeOS && hashId & 8) { + callback(e, hashId, keyCode); + if (e.defaultPrevented) + return; + else + hashId &= ~8; + } + if (!hashId && !(keyCode in keys.FUNCTION_KEYS) && !(keyCode in keys.PRINTABLE_KEYS)) { + return false; + } + + return callback(e, hashId, keyCode); +} + + +exports.addCommandKeyListener = function(el, callback) { + var addListener = exports.addListener; + if (useragent.isOldGecko || (useragent.isOpera && !("KeyboardEvent" in window))) { + var lastKeyDownKeyCode = null; + addListener(el, "keydown", function(e) { + lastKeyDownKeyCode = e.keyCode; + }); + addListener(el, "keypress", function(e) { + return normalizeCommandKeys(callback, e, lastKeyDownKeyCode); + }); + } else { + var lastDefaultPrevented = null; + + addListener(el, "keydown", function(e) { + pressedKeys[e.keyCode] = (pressedKeys[e.keyCode] || 0) + 1; + var result = normalizeCommandKeys(callback, e, e.keyCode); + lastDefaultPrevented = e.defaultPrevented; + return result; + }); + + addListener(el, "keypress", function(e) { + if (lastDefaultPrevented && (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey)) { + exports.stopEvent(e); + lastDefaultPrevented = null; + } + }); + + addListener(el, "keyup", function(e) { + pressedKeys[e.keyCode] = null; + }); + + if (!pressedKeys) { + resetPressedKeys(); + addListener(window, "focus", resetPressedKeys); + } + } +}; +function resetPressedKeys() { + pressedKeys = Object.create(null); +} + +if (typeof window == "object" && window.postMessage && !useragent.isOldIE) { + var postMessageId = 1; + exports.nextTick = function(callback, win) { + win = win || window; + var messageName = "zero-timeout-message-" + postMessageId; + exports.addListener(win, "message", function listener(e) { + if (e.data == messageName) { + exports.stopPropagation(e); + exports.removeListener(win, "message", listener); + callback(); + } + }); + win.postMessage(messageName, "*"); + }; +} + + +exports.nextFrame = typeof window == "object" && (window.requestAnimationFrame + || window.mozRequestAnimationFrame + || window.webkitRequestAnimationFrame + || window.msRequestAnimationFrame + || window.oRequestAnimationFrame); + +if (exports.nextFrame) + exports.nextFrame = exports.nextFrame.bind(window); +else + exports.nextFrame = function(callback) { + setTimeout(callback, 17); + }; +}); + +ace.define("ace/lib/lang",["require","exports","module"], function(require, exports, module) { +"use strict"; + +exports.last = function(a) { + return a[a.length - 1]; +}; + +exports.stringReverse = function(string) { + return string.split("").reverse().join(""); +}; + +exports.stringRepeat = function (string, count) { + var result = ''; + while (count > 0) { + if (count & 1) + result += string; + + if (count >>= 1) + string += string; + } + return result; +}; + +var trimBeginRegexp = /^\s\s*/; +var trimEndRegexp = /\s\s*$/; + +exports.stringTrimLeft = function (string) { + return string.replace(trimBeginRegexp, ''); +}; + +exports.stringTrimRight = function (string) { + return string.replace(trimEndRegexp, ''); +}; + +exports.copyObject = function(obj) { + var copy = {}; + for (var key in obj) { + copy[key] = obj[key]; + } + return copy; +}; + +exports.copyArray = function(array){ + var copy = []; + for (var i=0, l=array.length; i| Setting | Value |
|---|---|
| ", desc[option], " | "); + table.push(""); + renderOption(table, option, optionValues[option], editor.getOption(option)); + table.push(" |
\n\ + ${1}\n\ +\n\ +snippet body\n\ + \n\ + ${1}\n\ + \n\ +snippet br\n\ +
${1}\n\
+snippet col\n\
+ ${1}
\n\ +snippet param\n\ + ${3}\n\ +snippet pre\n\ +\n\
+ ${1}\n\
+ \n\
+snippet progress\n\
+ \n\
+snippet q\n\
+ ${1}\n\ +snippet rp\n\ + \n\ +snippet rt\n\ + \n\ +snippet ruby\n\ + \n\ + \n\ + \n\ +snippet s\n\ +
'; var_dump(${1}); echo '';\n\
+# pre_dump(); die();\n\
+snippet pdd\n\
+ echo ''; var_dump(${1}); echo ''; die(${2:});\n\
+snippet vd\n\
+ var_dump(${1});\n\
+snippet vdd\n\
+ var_dump(${1}); die(${2:});\n\
+snippet http_redirect\n\
+ header (\"HTTP/1.1 301 Moved Permanently\"); \n\
+ header (\"Location: \".URL); \n\
+ exit();\n\
+# Getters & Setters\n\
+snippet gs\n\
+ /**\n\
+ * Gets the value of ${1:foo}\n\
+ *\n\
+ * @return ${2:$1}\n\
+ */\n\
+ public function get${3:$2}()\n\
+ {\n\
+ return $this->${4:$1};\n\
+ }\n\
+\n\
+ /**\n\
+ * Sets the value of $1\n\
+ *\n\
+ * @param $2 $$1 ${5:description}\n\
+ *\n\
+ * @return ${6:$FILENAME}\n\
+ */\n\
+ public function set$3(${7:$2 }$$1)\n\
+ {\n\
+ $this->$4 = $$1;\n\
+ return $this;\n\
+ }${8}\n\
+# anotation, get, and set, useful for doctrine\n\
+snippet ags\n\
+ /**\n\
+ * ${1:description}\n\
+ * \n\
+ * @${7}\n\
+ */\n\
+ ${2:protected} $${3:foo};\n\
+\n\
+ public function get${4:$3}()\n\
+ {\n\
+ return $this->$3;\n\
+ }\n\
+\n\
+ public function set$4(${5:$4 }$${6:$3})\n\
+ {\n\
+ $this->$3 = $$6;\n\
+ return $this;\n\
+ }\n\
+snippet rett\n\
+ return true;\n\
+snippet retf\n\
+ return false;\n\
+";
+exports.scope = "php";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/plain_text.js b/services/web/public/js/ace-1.2.5/snippets/plain_text.js
new file mode 100644
index 0000000000..24223a6625
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/plain_text.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/plain_text",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "plain_text";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/powershell.js b/services/web/public/js/ace-1.2.5/snippets/powershell.js
new file mode 100644
index 0000000000..a8e7310a15
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/powershell.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/powershell",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "powershell";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/praat.js b/services/web/public/js/ace-1.2.5/snippets/praat.js
new file mode 100644
index 0000000000..dcf6826774
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/praat.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/praat",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "praat";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/prolog.js b/services/web/public/js/ace-1.2.5/snippets/prolog.js
new file mode 100644
index 0000000000..2d63cb83aa
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/prolog.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/prolog",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "prolog";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/properties.js b/services/web/public/js/ace-1.2.5/snippets/properties.js
new file mode 100644
index 0000000000..44c1ada78e
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/properties.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/properties",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "properties";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/protobuf.js b/services/web/public/js/ace-1.2.5/snippets/protobuf.js
new file mode 100644
index 0000000000..d00d57afd1
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/protobuf.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/protobuf",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "";
+exports.scope = "protobuf";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/python.js b/services/web/public/js/ace-1.2.5/snippets/python.js
new file mode 100644
index 0000000000..182b34067e
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/python.js
@@ -0,0 +1,165 @@
+ace.define("ace/snippets/python",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "snippet #!\n\
+ #!/usr/bin/env python\n\
+snippet imp\n\
+ import ${1:module}\n\
+snippet from\n\
+ from ${1:package} import ${2:module}\n\
+# Module Docstring\n\
+snippet docs\n\
+ '''\n\
+ File: ${1:FILENAME:file_name}\n\
+ Author: ${2:author}\n\
+ Description: ${3}\n\
+ '''\n\
+snippet wh\n\
+ while ${1:condition}:\n\
+ ${2:# TODO: write code...}\n\
+# dowh - does the same as do...while in other languages\n\
+snippet dowh\n\
+ while True:\n\
+ ${1:# TODO: write code...}\n\
+ if ${2:condition}:\n\
+ break\n\
+snippet with\n\
+ with ${1:expr} as ${2:var}:\n\
+ ${3:# TODO: write code...}\n\
+# New Class\n\
+snippet cl\n\
+ class ${1:ClassName}(${2:object}):\n\
+ \"\"\"${3:docstring for $1}\"\"\"\n\
+ def __init__(self, ${4:arg}):\n\
+ ${5:super($1, self).__init__()}\n\
+ self.$4 = $4\n\
+ ${6}\n\
+# New Function\n\
+snippet def\n\
+ def ${1:fname}(${2:`indent('.') ? 'self' : ''`}):\n\
+ \"\"\"${3:docstring for $1}\"\"\"\n\
+ ${4:# TODO: write code...}\n\
+snippet deff\n\
+ def ${1:fname}(${2:`indent('.') ? 'self' : ''`}):\n\
+ ${3:# TODO: write code...}\n\
+# New Method\n\
+snippet defs\n\
+ def ${1:mname}(self, ${2:arg}):\n\
+ ${3:# TODO: write code...}\n\
+# New Property\n\
+snippet property\n\
+ def ${1:foo}():\n\
+ doc = \"${2:The $1 property.}\"\n\
+ def fget(self):\n\
+ ${3:return self._$1}\n\
+ def fset(self, value):\n\
+ ${4:self._$1 = value}\n\
+# Ifs\n\
+snippet if\n\
+ if ${1:condition}:\n\
+ ${2:# TODO: write code...}\n\
+snippet el\n\
+ else:\n\
+ ${1:# TODO: write code...}\n\
+snippet ei\n\
+ elif ${1:condition}:\n\
+ ${2:# TODO: write code...}\n\
+# For\n\
+snippet for\n\
+ for ${1:item} in ${2:items}:\n\
+ ${3:# TODO: write code...}\n\
+# Encodes\n\
+snippet cutf8\n\
+ # -*- coding: utf-8 -*-\n\
+snippet clatin1\n\
+ # -*- coding: latin-1 -*-\n\
+snippet cascii\n\
+ # -*- coding: ascii -*-\n\
+# Lambda\n\
+snippet ld\n\
+ ${1:var} = lambda ${2:vars} : ${3:action}\n\
+snippet .\n\
+ self.\n\
+snippet try Try/Except\n\
+ try:\n\
+ ${1:# TODO: write code...}\n\
+ except ${2:Exception}, ${3:e}:\n\
+ ${4:raise $3}\n\
+snippet try Try/Except/Else\n\
+ try:\n\
+ ${1:# TODO: write code...}\n\
+ except ${2:Exception}, ${3:e}:\n\
+ ${4:raise $3}\n\
+ else:\n\
+ ${5:# TODO: write code...}\n\
+snippet try Try/Except/Finally\n\
+ try:\n\
+ ${1:# TODO: write code...}\n\
+ except ${2:Exception}, ${3:e}:\n\
+ ${4:raise $3}\n\
+ finally:\n\
+ ${5:# TODO: write code...}\n\
+snippet try Try/Except/Else/Finally\n\
+ try:\n\
+ ${1:# TODO: write code...}\n\
+ except ${2:Exception}, ${3:e}:\n\
+ ${4:raise $3}\n\
+ else:\n\
+ ${5:# TODO: write code...}\n\
+ finally:\n\
+ ${6:# TODO: write code...}\n\
+# if __name__ == '__main__':\n\
+snippet ifmain\n\
+ if __name__ == '__main__':\n\
+ ${1:main()}\n\
+# __magic__\n\
+snippet _\n\
+ __${1:init}__${2}\n\
+# python debugger (pdb)\n\
+snippet pdb\n\
+ import pdb; pdb.set_trace()\n\
+# ipython debugger (ipdb)\n\
+snippet ipdb\n\
+ import ipdb; ipdb.set_trace()\n\
+# ipython debugger (pdbbb)\n\
+snippet pdbbb\n\
+ import pdbpp; pdbpp.set_trace()\n\
+snippet pprint\n\
+ import pprint; pprint.pprint(${1})${2}\n\
+snippet \"\n\
+ \"\"\"\n\
+ ${1:doc}\n\
+ \"\"\"\n\
+# test function/method\n\
+snippet test\n\
+ def test_${1:description}(${2:self}):\n\
+ ${3:# TODO: write code...}\n\
+# test case\n\
+snippet testcase\n\
+ class ${1:ExampleCase}(unittest.TestCase):\n\
+ \n\
+ def test_${2:description}(self):\n\
+ ${3:# TODO: write code...}\n\
+snippet fut\n\
+ from __future__ import ${1}\n\
+#getopt\n\
+snippet getopt\n\
+ try:\n\
+ # Short option syntax: \"hv:\"\n\
+ # Long option syntax: \"help\" or \"verbose=\"\n\
+ opts, args = getopt.getopt(sys.argv[1:], \"${1:short_options}\", [${2:long_options}])\n\
+ \n\
+ except getopt.GetoptError, err:\n\
+ # Print debug info\n\
+ print str(err)\n\
+ ${3:error_action}\n\
+\n\
+ for option, argument in opts:\n\
+ if option in (\"-h\", \"--help\"):\n\
+ ${4}\n\
+ elif option in (\"-v\", \"--verbose\"):\n\
+ verbose = argument\n\
+";
+exports.scope = "python";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/r.js b/services/web/public/js/ace-1.2.5/snippets/r.js
new file mode 100644
index 0000000000..24c02a0c6a
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/r.js
@@ -0,0 +1,128 @@
+ace.define("ace/snippets/r",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "snippet #!\n\
+ #!/usr/bin/env Rscript\n\
+\n\
+# includes\n\
+snippet lib\n\
+ library(${1:package})\n\
+snippet req\n\
+ require(${1:package})\n\
+snippet source\n\
+ source('${1:file}')\n\
+\n\
+# conditionals\n\
+snippet if\n\
+ if (${1:condition}) {\n\
+ ${2:code}\n\
+ }\n\
+snippet el\n\
+ else {\n\
+ ${1:code}\n\
+ }\n\
+snippet ei\n\
+ else if (${1:condition}) {\n\
+ ${2:code}\n\
+ }\n\
+\n\
+# functions\n\
+snippet fun\n\
+ ${1:name} = function (${2:variables}) {\n\
+ ${3:code}\n\
+ }\n\
+snippet ret\n\
+ return(${1:code})\n\
+\n\
+# dataframes, lists, etc\n\
+snippet df\n\
+ ${1:name}[${2:rows}, ${3:cols}]\n\
+snippet c\n\
+ c(${1:items})\n\
+snippet li\n\
+ list(${1:items})\n\
+snippet mat\n\
+ matrix(${1:data}, nrow=${2:rows}, ncol=${3:cols})\n\
+\n\
+# apply functions\n\
+snippet apply\n\
+ apply(${1:array}, ${2:margin}, ${3:function})\n\
+snippet lapply\n\
+ lapply(${1:list}, ${2:function})\n\
+snippet sapply\n\
+ sapply(${1:list}, ${2:function})\n\
+snippet vapply\n\
+ vapply(${1:list}, ${2:function}, ${3:type})\n\
+snippet mapply\n\
+ mapply(${1:function}, ${2:...})\n\
+snippet tapply\n\
+ tapply(${1:vector}, ${2:index}, ${3:function})\n\
+snippet rapply\n\
+ rapply(${1:list}, ${2:function})\n\
+\n\
+# plyr functions\n\
+snippet dd\n\
+ ddply(${1:frame}, ${2:variables}, ${3:function})\n\
+snippet dl\n\
+ dlply(${1:frame}, ${2:variables}, ${3:function})\n\
+snippet da\n\
+ daply(${1:frame}, ${2:variables}, ${3:function})\n\
+snippet d_\n\
+ d_ply(${1:frame}, ${2:variables}, ${3:function})\n\
+\n\
+snippet ad\n\
+ adply(${1:array}, ${2:margin}, ${3:function})\n\
+snippet al\n\
+ alply(${1:array}, ${2:margin}, ${3:function})\n\
+snippet aa\n\
+ aaply(${1:array}, ${2:margin}, ${3:function})\n\
+snippet a_\n\
+ a_ply(${1:array}, ${2:margin}, ${3:function})\n\
+\n\
+snippet ld\n\
+ ldply(${1:list}, ${2:function})\n\
+snippet ll\n\
+ llply(${1:list}, ${2:function})\n\
+snippet la\n\
+ laply(${1:list}, ${2:function})\n\
+snippet l_\n\
+ l_ply(${1:list}, ${2:function})\n\
+\n\
+snippet md\n\
+ mdply(${1:matrix}, ${2:function})\n\
+snippet ml\n\
+ mlply(${1:matrix}, ${2:function})\n\
+snippet ma\n\
+ maply(${1:matrix}, ${2:function})\n\
+snippet m_\n\
+ m_ply(${1:matrix}, ${2:function})\n\
+\n\
+# plot functions\n\
+snippet pl\n\
+ plot(${1:x}, ${2:y})\n\
+snippet ggp\n\
+ ggplot(${1:data}, aes(${2:aesthetics}))\n\
+snippet img\n\
+ ${1:(jpeg,bmp,png,tiff)}(filename=\"${2:filename}\", width=${3}, height=${4}, unit=\"${5}\")\n\
+ ${6:plot}\n\
+ dev.off()\n\
+\n\
+# statistical test functions\n\
+snippet fis\n\
+ fisher.test(${1:x}, ${2:y})\n\
+snippet chi\n\
+ chisq.test(${1:x}, ${2:y})\n\
+snippet tt\n\
+ t.test(${1:x}, ${2:y})\n\
+snippet wil\n\
+ wilcox.test(${1:x}, ${2:y})\n\
+snippet cor\n\
+ cor.test(${1:x}, ${2:y})\n\
+snippet fte\n\
+ var.test(${1:x}, ${2:y})\n\
+snippet kvt \n\
+ kv.test(${1:x}, ${2:y})\n\
+";
+exports.scope = "r";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/razor.js b/services/web/public/js/ace-1.2.5/snippets/razor.js
new file mode 100644
index 0000000000..78fdf8c3ec
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/razor.js
@@ -0,0 +1,10 @@
+ace.define("ace/snippets/razor",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "snippet if\n\
+(${1} == ${2}) {\n\
+ ${3}\n\
+}";
+exports.scope = "razor";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/rdoc.js b/services/web/public/js/ace-1.2.5/snippets/rdoc.js
new file mode 100644
index 0000000000..956de47aa2
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/rdoc.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/rdoc",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "rdoc";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/rhtml.js b/services/web/public/js/ace-1.2.5/snippets/rhtml.js
new file mode 100644
index 0000000000..e62ce87f74
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/rhtml.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/rhtml",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "rhtml";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/rst.js b/services/web/public/js/ace-1.2.5/snippets/rst.js
new file mode 100644
index 0000000000..db6c960f63
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/rst.js
@@ -0,0 +1,29 @@
+ace.define("ace/snippets/rst",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# rst\n\
+\n\
+snippet :\n\
+ :${1:field name}: ${2:field body}\n\
+snippet *\n\
+ *${1:Emphasis}*\n\
+snippet **\n\
+ **${1:Strong emphasis}**\n\
+snippet _\n\
+ \\`${1:hyperlink-name}\\`_\n\
+ .. _\\`$1\\`: ${2:link-block}\n\
+snippet =\n\
+ ${1:Title}\n\
+ =====${2:=}\n\
+ ${3}\n\
+snippet -\n\
+ ${1:Title}\n\
+ -----${2:-}\n\
+ ${3}\n\
+snippet cont:\n\
+ .. contents::\n\
+ \n\
+";
+exports.scope = "rst";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/ruby.js b/services/web/public/js/ace-1.2.5/snippets/ruby.js
new file mode 100644
index 0000000000..18bc409f47
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/ruby.js
@@ -0,0 +1,935 @@
+ace.define("ace/snippets/ruby",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "########################################\n\
+# Ruby snippets - for Rails, see below #\n\
+########################################\n\
+\n\
+# encoding for Ruby 1.9\n\
+snippet enc\n\
+ # encoding: utf-8\n\
+\n\
+# #!/usr/bin/env ruby\n\
+snippet #!\n\
+ #!/usr/bin/env ruby\n\
+ # encoding: utf-8\n\
+\n\
+# New Block\n\
+snippet =b\n\
+ =begin rdoc\n\
+ ${1}\n\
+ =end\n\
+snippet y\n\
+ :yields: ${1:arguments}\n\
+snippet rb\n\
+ #!/usr/bin/env ruby -wKU\n\
+snippet beg\n\
+ begin\n\
+ ${3}\n\
+ rescue ${1:Exception} => ${2:e}\n\
+ end\n\
+\n\
+snippet req require\n\
+ require \"${1}\"${2}\n\
+snippet #\n\
+ # =>\n\
+snippet end\n\
+ __END__\n\
+snippet case\n\
+ case ${1:object}\n\
+ when ${2:condition}\n\
+ ${3}\n\
+ end\n\
+snippet when\n\
+ when ${1:condition}\n\
+ ${2}\n\
+snippet def\n\
+ def ${1:method_name}\n\
+ ${2}\n\
+ end\n\
+snippet deft\n\
+ def test_${1:case_name}\n\
+ ${2}\n\
+ end\n\
+snippet if\n\
+ if ${1:condition}\n\
+ ${2}\n\
+ end\n\
+snippet ife\n\
+ if ${1:condition}\n\
+ ${2}\n\
+ else\n\
+ ${3}\n\
+ end\n\
+snippet elsif\n\
+ elsif ${1:condition}\n\
+ ${2}\n\
+snippet unless\n\
+ unless ${1:condition}\n\
+ ${2}\n\
+ end\n\
+snippet while\n\
+ while ${1:condition}\n\
+ ${2}\n\
+ end\n\
+snippet for\n\
+ for ${1:e} in ${2:c}\n\
+ ${3}\n\
+ end\n\
+snippet until\n\
+ until ${1:condition}\n\
+ ${2}\n\
+ end\n\
+snippet cla class .. end\n\
+ class ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`}\n\
+ ${2}\n\
+ end\n\
+snippet cla class .. initialize .. end\n\
+ class ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`}\n\
+ def initialize(${2:args})\n\
+ ${3}\n\
+ end\n\
+ end\n\
+snippet cla class .. < ParentClass .. initialize .. end\n\
+ class ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`} < ${2:ParentClass}\n\
+ def initialize(${3:args})\n\
+ ${4}\n\
+ end\n\
+ end\n\
+snippet cla ClassName = Struct .. do .. end\n\
+ ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`} = Struct.new(:${2:attr_names}) do\n\
+ def ${3:method_name}\n\
+ ${4}\n\
+ end\n\
+ end\n\
+snippet cla class BlankSlate .. initialize .. end\n\
+ class ${1:BlankSlate}\n\
+ instance_methods.each { |meth| undef_method(meth) unless meth =~ /\\A__/ }\n\
+ end\n\
+snippet cla class << self .. end\n\
+ class << ${1:self}\n\
+ ${2}\n\
+ end\n\
+# class .. < DelegateClass .. initialize .. end\n\
+snippet cla-\n\
+ class ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`} < DelegateClass(${2:ParentClass})\n\
+ def initialize(${3:args})\n\
+ super(${4:del_obj})\n\
+\n\
+ ${5}\n\
+ end\n\
+ end\n\
+snippet mod module .. end\n\
+ module ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`}\n\
+ ${2}\n\
+ end\n\
+snippet mod module .. module_function .. end\n\
+ module ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`}\n\
+ module_function\n\
+\n\
+ ${2}\n\
+ end\n\
+snippet mod module .. ClassMethods .. end\n\
+ module ${1:`substitute(Filename(), '\\(_\\|^\\)\\(.\\)', '\\u\\2', 'g')`}\n\
+ module ClassMethods\n\
+ ${2}\n\
+ end\n\
+\n\
+ module InstanceMethods\n\
+\n\
+ end\n\
+\n\
+ def self.included(receiver)\n\
+ receiver.extend ClassMethods\n\
+ receiver.send :include, InstanceMethods\n\
+ end\n\
+ end\n\
+# attr_reader\n\
+snippet r\n\
+ attr_reader :${1:attr_names}\n\
+# attr_writer\n\
+snippet w\n\
+ attr_writer :${1:attr_names}\n\
+# attr_accessor\n\
+snippet rw\n\
+ attr_accessor :${1:attr_names}\n\
+snippet atp\n\
+ attr_protected :${1:attr_names}\n\
+snippet ata\n\
+ attr_accessible :${1:attr_names}\n\
+# include Enumerable\n\
+snippet Enum\n\
+ include Enumerable\n\
+\n\
+ def each(&block)\n\
+ ${1}\n\
+ end\n\
+# include Comparable\n\
+snippet Comp\n\
+ include Comparable\n\
+\n\
+ def <=>(other)\n\
+ ${1}\n\
+ end\n\
+# extend Forwardable\n\
+snippet Forw-\n\
+ extend Forwardable\n\
+# def self\n\
+snippet defs\n\
+ def self.${1:class_method_name}\n\
+ ${2}\n\
+ end\n\
+# def method_missing\n\
+snippet defmm\n\
+ def method_missing(meth, *args, &blk)\n\
+ ${1}\n\
+ end\n\
+snippet defd\n\
+ def_delegator :${1:@del_obj}, :${2:del_meth}, :${3:new_name}\n\
+snippet defds\n\
+ def_delegators :${1:@del_obj}, :${2:del_methods}\n\
+snippet am\n\
+ alias_method :${1:new_name}, :${2:old_name}\n\
+snippet app\n\
+ if __FILE__ == $PROGRAM_NAME\n\
+ ${1}\n\
+ end\n\
+# usage_if()\n\
+snippet usai\n\
+ if ARGV.${1}\n\
+ abort \"Usage: #{$PROGRAM_NAME} ${2:ARGS_GO_HERE}\"${3}\n\
+ end\n\
+# usage_unless()\n\
+snippet usau\n\
+ unless ARGV.${1}\n\
+ abort \"Usage: #{$PROGRAM_NAME} ${2:ARGS_GO_HERE}\"${3}\n\
+ end\n\
+snippet array\n\
+ Array.new(${1:10}) { |${2:i}| ${3} }\n\
+snippet hash\n\
+ Hash.new { |${1:hash}, ${2:key}| $1[$2] = ${3} }\n\
+snippet file File.foreach() { |line| .. }\n\
+ File.foreach(${1:\"path/to/file\"}) { |${2:line}| ${3} }\n\
+snippet file File.read()\n\
+ File.read(${1:\"path/to/file\"})${2}\n\
+snippet Dir Dir.global() { |file| .. }\n\
+ Dir.glob(${1:\"dir/glob/*\"}) { |${2:file}| ${3} }\n\
+snippet Dir Dir[\"..\"]\n\
+ Dir[${1:\"glob/**/*.rb\"}]${2}\n\
+snippet dir\n\
+ Filename.dirname(__FILE__)\n\
+snippet deli\n\
+ delete_if { |${1:e}| ${2} }\n\
+snippet fil\n\
+ fill(${1:range}) { |${2:i}| ${3} }\n\
+# flatten_once()\n\
+snippet flao\n\
+ inject(Array.new) { |${1:arr}, ${2:a}| $1.push(*$2)}${3}\n\
+snippet zip\n\
+ zip(${1:enums}) { |${2:row}| ${3} }\n\
+# downto(0) { |n| .. }\n\
+snippet dow\n\
+ downto(${1:0}) { |${2:n}| ${3} }\n\
+snippet ste\n\
+ step(${1:2}) { |${2:n}| ${3} }\n\
+snippet tim\n\
+ times { |${1:n}| ${2} }\n\
+snippet upt\n\
+ upto(${1:1.0/0.0}) { |${2:n}| ${3} }\n\
+snippet loo\n\
+ loop { ${1} }\n\
+snippet ea\n\
+ each { |${1:e}| ${2} }\n\
+snippet ead\n\
+ each do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet eab\n\
+ each_byte { |${1:byte}| ${2} }\n\
+snippet eac- each_char { |chr| .. }\n\
+ each_char { |${1:chr}| ${2} }\n\
+snippet eac- each_cons(..) { |group| .. }\n\
+ each_cons(${1:2}) { |${2:group}| ${3} }\n\
+snippet eai\n\
+ each_index { |${1:i}| ${2} }\n\
+snippet eaid\n\
+ each_index do |${1:i}|\n\
+ ${2}\n\
+ end\n\
+snippet eak\n\
+ each_key { |${1:key}| ${2} }\n\
+snippet eakd\n\
+ each_key do |${1:key}|\n\
+ ${2}\n\
+ end\n\
+snippet eal\n\
+ each_line { |${1:line}| ${2} }\n\
+snippet eald\n\
+ each_line do |${1:line}|\n\
+ ${2}\n\
+ end\n\
+snippet eap\n\
+ each_pair { |${1:name}, ${2:val}| ${3} }\n\
+snippet eapd\n\
+ each_pair do |${1:name}, ${2:val}|\n\
+ ${3}\n\
+ end\n\
+snippet eas-\n\
+ each_slice(${1:2}) { |${2:group}| ${3} }\n\
+snippet easd-\n\
+ each_slice(${1:2}) do |${2:group}|\n\
+ ${3}\n\
+ end\n\
+snippet eav\n\
+ each_value { |${1:val}| ${2} }\n\
+snippet eavd\n\
+ each_value do |${1:val}|\n\
+ ${2}\n\
+ end\n\
+snippet eawi\n\
+ each_with_index { |${1:e}, ${2:i}| ${3} }\n\
+snippet eawid\n\
+ each_with_index do |${1:e},${2:i}|\n\
+ ${3}\n\
+ end\n\
+snippet reve\n\
+ reverse_each { |${1:e}| ${2} }\n\
+snippet reved\n\
+ reverse_each do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet inj\n\
+ inject(${1:init}) { |${2:mem}, ${3:var}| ${4} }\n\
+snippet injd\n\
+ inject(${1:init}) do |${2:mem}, ${3:var}|\n\
+ ${4}\n\
+ end\n\
+snippet map\n\
+ map { |${1:e}| ${2} }\n\
+snippet mapd\n\
+ map do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet mapwi-\n\
+ enum_with_index.map { |${1:e}, ${2:i}| ${3} }\n\
+snippet sor\n\
+ sort { |a, b| ${1} }\n\
+snippet sorb\n\
+ sort_by { |${1:e}| ${2} }\n\
+snippet ran\n\
+ sort_by { rand }\n\
+snippet all\n\
+ all? { |${1:e}| ${2} }\n\
+snippet any\n\
+ any? { |${1:e}| ${2} }\n\
+snippet cl\n\
+ classify { |${1:e}| ${2} }\n\
+snippet col\n\
+ collect { |${1:e}| ${2} }\n\
+snippet cold\n\
+ collect do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet det\n\
+ detect { |${1:e}| ${2} }\n\
+snippet detd\n\
+ detect do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet fet\n\
+ fetch(${1:name}) { |${2:key}| ${3} }\n\
+snippet fin\n\
+ find { |${1:e}| ${2} }\n\
+snippet find\n\
+ find do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet fina\n\
+ find_all { |${1:e}| ${2} }\n\
+snippet finad\n\
+ find_all do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet gre\n\
+ grep(${1:/pattern/}) { |${2:match}| ${3} }\n\
+snippet sub\n\
+ ${1:g}sub(${2:/pattern/}) { |${3:match}| ${4} }\n\
+snippet sca\n\
+ scan(${1:/pattern/}) { |${2:match}| ${3} }\n\
+snippet scad\n\
+ scan(${1:/pattern/}) do |${2:match}|\n\
+ ${3}\n\
+ end\n\
+snippet max\n\
+ max { |a, b| ${1} }\n\
+snippet min\n\
+ min { |a, b| ${1} }\n\
+snippet par\n\
+ partition { |${1:e}| ${2} }\n\
+snippet pard\n\
+ partition do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet rej\n\
+ reject { |${1:e}| ${2} }\n\
+snippet rejd\n\
+ reject do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet sel\n\
+ select { |${1:e}| ${2} }\n\
+snippet seld\n\
+ select do |${1:e}|\n\
+ ${2}\n\
+ end\n\
+snippet lam\n\
+ lambda { |${1:args}| ${2} }\n\
+snippet doo\n\
+ do\n\
+ ${1}\n\
+ end\n\
+snippet dov\n\
+ do |${1:variable}|\n\
+ ${2}\n\
+ end\n\
+snippet :\n\
+ :${1:key} => ${2:\"value\"}${3}\n\
+snippet ope\n\
+ open(${1:\"path/or/url/or/pipe\"}, \"${2:w}\") { |${3:io}| ${4} }\n\
+# path_from_here()\n\
+snippet fpath\n\
+ File.join(File.dirname(__FILE__), *%2[${1:rel path here}])${2}\n\
+# unix_filter {}\n\
+snippet unif\n\
+ ARGF.each_line${1} do |${2:line}|\n\
+ ${3}\n\
+ end\n\
+# option_parse {}\n\
+snippet optp\n\
+ require \"optparse\"\n\
+\n\
+ options = {${1:default => \"args\"}}\n\
+\n\
+ ARGV.options do |opts|\n\
+ opts.banner = \"Usage: #{File.basename($PROGRAM_NAME)}\n\
+snippet opt\n\
+ opts.on( \"-${1:o}\", \"--${2:long-option-name}\", ${3:String},\n\
+ \"${4:Option description.}\") do |${5:opt}|\n\
+ ${6}\n\
+ end\n\
+snippet tc\n\
+ require \"test/unit\"\n\
+\n\
+ require \"${1:library_file_name}\"\n\
+\n\
+ class Test${2:$1} < Test::Unit::TestCase\n\
+ def test_${3:case_name}\n\
+ ${4}\n\
+ end\n\
+ end\n\
+snippet ts\n\
+ require \"test/unit\"\n\
+\n\
+ require \"tc_${1:test_case_file}\"\n\
+ require \"tc_${2:test_case_file}\"${3}\n\
+snippet as\n\
+ assert ${1:test}, \"${2:Failure message.}\"${3}\n\
+snippet ase\n\
+ assert_equal ${1:expected}, ${2:actual}${3}\n\
+snippet asne\n\
+ assert_not_equal ${1:unexpected}, ${2:actual}${3}\n\
+snippet asid\n\
+ assert_in_delta ${1:expected_float}, ${2:actual_float}, ${3:2 ** -20}${4}\n\
+snippet asio\n\
+ assert_instance_of ${1:ExpectedClass}, ${2:actual_instance}${3}\n\
+snippet asko\n\
+ assert_kind_of ${1:ExpectedKind}, ${2:actual_instance}${3}\n\
+snippet asn\n\
+ assert_nil ${1:instance}${2}\n\
+snippet asnn\n\
+ assert_not_nil ${1:instance}${2}\n\
+snippet asm\n\
+ assert_match /${1:expected_pattern}/, ${2:actual_string}${3}\n\
+snippet asnm\n\
+ assert_no_match /${1:unexpected_pattern}/, ${2:actual_string}${3}\n\
+snippet aso\n\
+ assert_operator ${1:left}, :${2:operator}, ${3:right}${4}\n\
+snippet asr\n\
+ assert_raise ${1:Exception} { ${2} }\n\
+snippet asrd\n\
+ assert_raise ${1:Exception} do\n\
+ ${2}\n\
+ end\n\
+snippet asnr\n\
+ assert_nothing_raised ${1:Exception} { ${2} }\n\
+snippet asnrd\n\
+ assert_nothing_raised ${1:Exception} do\n\
+ ${2}\n\
+ end\n\
+snippet asrt\n\
+ assert_respond_to ${1:object}, :${2:method}${3}\n\
+snippet ass assert_same(..)\n\
+ assert_same ${1:expected}, ${2:actual}${3}\n\
+snippet ass assert_send(..)\n\
+ assert_send [${1:object}, :${2:message}, ${3:args}]${4}\n\
+snippet asns\n\
+ assert_not_same ${1:unexpected}, ${2:actual}${3}\n\
+snippet ast\n\
+ assert_throws :${1:expected} { ${2} }\n\
+snippet astd\n\
+ assert_throws :${1:expected} do\n\
+ ${2}\n\
+ end\n\
+snippet asnt\n\
+ assert_nothing_thrown { ${1} }\n\
+snippet asntd\n\
+ assert_nothing_thrown do\n\
+ ${1}\n\
+ end\n\
+snippet fl\n\
+ flunk \"${1:Failure message.}\"${2}\n\
+# Benchmark.bmbm do .. end\n\
+snippet bm-\n\
+ TESTS = ${1:10_000}\n\
+ Benchmark.bmbm do |results|\n\
+ ${2}\n\
+ end\n\
+snippet rep\n\
+ results.report(\"${1:name}:\") { TESTS.times { ${2} }}\n\
+# Marshal.dump(.., file)\n\
+snippet Md\n\
+ File.open(${1:\"path/to/file.dump\"}, \"wb\") { |${2:file}| Marshal.dump(${3:obj}, $2) }${4}\n\
+# Mashal.load(obj)\n\
+snippet Ml\n\
+ File.open(${1:\"path/to/file.dump\"}, \"rb\") { |${2:file}| Marshal.load($2) }${3}\n\
+# deep_copy(..)\n\
+snippet deec\n\
+ Marshal.load(Marshal.dump(${1:obj_to_copy}))${2}\n\
+snippet Pn-\n\
+ PStore.new(${1:\"file_name.pstore\"})${2}\n\
+snippet tra\n\
+ transaction(${1:true}) { ${2} }\n\
+# xmlread(..)\n\
+snippet xml-\n\
+ REXML::Document.new(File.read(${1:\"path/to/file\"}))${2}\n\
+# xpath(..) { .. }\n\
+snippet xpa\n\
+ elements.each(${1:\"//Xpath\"}) do |${2:node}|\n\
+ ${3}\n\
+ end\n\
+# class_from_name()\n\
+snippet clafn\n\
+ split(\"::\").inject(Object) { |par, const| par.const_get(const) }\n\
+# singleton_class()\n\
+snippet sinc\n\
+ class << self; self end\n\
+snippet nam\n\
+ namespace :${1:`Filename()`} do\n\
+ ${2}\n\
+ end\n\
+snippet tas\n\
+ desc \"${1:Task description}\"\n\
+ task :${2:task_name => [:dependent, :tasks]} do\n\
+ ${3}\n\
+ end\n\
+# block\n\
+snippet b\n\
+ { |${1:var}| ${2} }\n\
+snippet begin\n\
+ begin\n\
+ raise 'A test exception.'\n\
+ rescue Exception => e\n\
+ puts e.message\n\
+ puts e.backtrace.inspect\n\
+ else\n\
+ # other exception\n\
+ ensure\n\
+ # always executed\n\
+ end\n\
+\n\
+#debugging\n\
+snippet debug\n\
+ require 'ruby-debug'; debugger; true;\n\
+snippet pry\n\
+ require 'pry'; binding.pry\n\
+\n\
+#############################################\n\
+# Rails snippets - for pure Ruby, see above #\n\
+#############################################\n\
+snippet art\n\
+ assert_redirected_to ${1::action => \"${2:index}\"}\n\
+snippet artnp\n\
+ assert_redirected_to ${1:parent}_${2:child}_path(${3:@$1}, ${4:@$2})\n\
+snippet artnpp\n\
+ assert_redirected_to ${1:parent}_${2:child}_path(${3:@$1})\n\
+snippet artp\n\
+ assert_redirected_to ${1:model}_path(${2:@$1})\n\
+snippet artpp\n\
+ assert_redirected_to ${1:model}s_path\n\
+snippet asd\n\
+ assert_difference \"${1:Model}.${2:count}\", $1 do\n\
+ ${3}\n\
+ end\n\
+snippet asnd\n\
+ assert_no_difference \"${1:Model}.${2:count}\" do\n\
+ ${3}\n\
+ end\n\
+snippet asre\n\
+ assert_response :${1:success}, @response.body${2}\n\
+snippet asrj\n\
+ assert_rjs :${1:replace}, \"${2:dom id}\"\n\
+snippet ass assert_select(..)\n\
+ assert_select '${1:path}', :${2:text} => '${3:inner_html' ${4:do}\n\
+snippet bf\n\
+ before_filter :${1:method}\n\
+snippet bt\n\
+ belongs_to :${1:association}\n\
+snippet crw\n\
+ cattr_accessor :${1:attr_names}\n\
+snippet defcreate\n\
+ def create\n\
+ @${1:model_class_name} = ${2:ModelClassName}.new(params[:$1])\n\
+\n\
+ respond_to do |wants|\n\
+ if @$1.save\n\
+ flash[:notice] = '$2 was successfully created.'\n\
+ wants.html { redirect_to(@$1) }\n\
+ wants.xml { render :xml => @$1, :status => :created, :location => @$1 }\n\
+ else\n\
+ wants.html { render :action => \"new\" }\n\
+ wants.xml { render :xml => @$1.errors, :status => :unprocessable_entity }\n\
+ end\n\
+ end\n\
+ end${3}\n\
+snippet defdestroy\n\
+ def destroy\n\
+ @${1:model_class_name} = ${2:ModelClassName}.find(params[:id])\n\
+ @$1.destroy\n\
+\n\
+ respond_to do |wants|\n\
+ wants.html { redirect_to($1s_url) }\n\
+ wants.xml { head :ok }\n\
+ end\n\
+ end${3}\n\
+snippet defedit\n\
+ def edit\n\
+ @${1:model_class_name} = ${2:ModelClassName}.find(params[:id])\n\
+ end\n\
+snippet defindex\n\
+ def index\n\
+ @${1:model_class_name} = ${2:ModelClassName}.all\n\
+\n\
+ respond_to do |wants|\n\
+ wants.html # index.html.erb\n\
+ wants.xml { render :xml => @$1s }\n\
+ end\n\
+ end${3}\n\
+snippet defnew\n\
+ def new\n\
+ @${1:model_class_name} = ${2:ModelClassName}.new\n\
+\n\
+ respond_to do |wants|\n\
+ wants.html # new.html.erb\n\
+ wants.xml { render :xml => @$1 }\n\
+ end\n\
+ end${3}\n\
+snippet defshow\n\
+ def show\n\
+ @${1:model_class_name} = ${2:ModelClassName}.find(params[:id])\n\
+\n\
+ respond_to do |wants|\n\
+ wants.html # show.html.erb\n\
+ wants.xml { render :xml => @$1 }\n\
+ end\n\
+ end${3}\n\
+snippet defupdate\n\
+ def update\n\
+ @${1:model_class_name} = ${2:ModelClassName}.find(params[:id])\n\
+\n\
+ respond_to do |wants|\n\
+ if @$1.update_attributes(params[:$1])\n\
+ flash[:notice] = '$2 was successfully updated.'\n\
+ wants.html { redirect_to(@$1) }\n\
+ wants.xml { head :ok }\n\
+ else\n\
+ wants.html { render :action => \"edit\" }\n\
+ wants.xml { render :xml => @$1.errors, :status => :unprocessable_entity }\n\
+ end\n\
+ end\n\
+ end${3}\n\
+snippet flash\n\
+ flash[:${1:notice}] = \"${2}\"\n\
+snippet habtm\n\
+ has_and_belongs_to_many :${1:object}, :join_table => \"${2:table_name}\", :foreign_key => \"${3}_id\"${4}\n\
+snippet hm\n\
+ has_many :${1:object}\n\
+snippet hmd\n\
+ has_many :${1:other}s, :class_name => \"${2:$1}\", :foreign_key => \"${3:$1}_id\", :dependent => :destroy${4}\n\
+snippet hmt\n\
+ has_many :${1:object}, :through => :${2:object}\n\
+snippet ho\n\
+ has_one :${1:object}\n\
+snippet i18\n\
+ I18n.t('${1:type.key}')${2}\n\
+snippet ist\n\
+ <%= image_submit_tag(\"${1:agree.png}\", :id => \"${2:id}\"${3} %>\n\
+snippet log\n\
+ Rails.logger.${1:debug} ${2}\n\
+snippet log2\n\
+ RAILS_DEFAULT_LOGGER.${1:debug} ${2}\n\
+snippet logd\n\
+ logger.debug { \"${1:message}\" }${2}\n\
+snippet loge\n\
+ logger.error { \"${1:message}\" }${2}\n\
+snippet logf\n\
+ logger.fatal { \"${1:message}\" }${2}\n\
+snippet logi\n\
+ logger.info { \"${1:message}\" }${2}\n\
+snippet logw\n\
+ logger.warn { \"${1:message}\" }${2}\n\
+snippet mapc\n\
+ ${1:map}.${2:connect} '${3:controller/:action/:id}'\n\
+snippet mapca\n\
+ ${1:map}.catch_all \"*${2:anything}\", :controller => \"${3:default}\", :action => \"${4:error}\"${5}\n\
+snippet mapr\n\
+ ${1:map}.resource :${2:resource}\n\
+snippet maprs\n\
+ ${1:map}.resources :${2:resource}\n\
+snippet mapwo\n\
+ ${1:map}.with_options :${2:controller} => '${3:thing}' do |$3|\n\
+ ${4}\n\
+ end\n\
+snippet mbs\n\
+ before_save :${1:method}\n\
+snippet mcht\n\
+ change_table :${1:table_name} do |t|\n\
+ ${2}\n\
+ end\n\
+snippet mp\n\
+ map(&:${1:id})\n\
+snippet mrw\n\
+ mattr_accessor :${1:attr_names}\n\
+snippet oa\n\
+ order(\"${1:field}\")\n\
+snippet od\n\
+ order(\"${1:field} DESC\")\n\
+snippet pa\n\
+ params[:${1:id}]${2}\n\
+snippet ra\n\
+ render :action => \"${1:action}\"\n\
+snippet ral\n\
+ render :action => \"${1:action}\", :layout => \"${2:layoutname}\"\n\
+snippet rest\n\
+ respond_to do |wants|\n\
+ wants.${1:html} { ${2} }\n\
+ end\n\
+snippet rf\n\
+ render :file => \"${1:filepath}\"\n\
+snippet rfu\n\
+ render :file => \"${1:filepath}\", :use_full_path => ${2:false}\n\
+snippet ri\n\
+ render :inline => \"${1:<%= 'hello' %>}\"\n\
+snippet ril\n\
+ render :inline => \"${1:<%= 'hello' %>}\", :locals => { ${2::name} => \"${3:value}\"${4} }\n\
+snippet rit\n\
+ render :inline => \"${1:<%= 'hello' %>}\", :type => ${2::rxml}\n\
+snippet rjson\n\
+ render :json => ${1:text to render}\n\
+snippet rl\n\
+ render :layout => \"${1:layoutname}\"\n\
+snippet rn\n\
+ render :nothing => ${1:true}\n\
+snippet rns\n\
+ render :nothing => ${1:true}, :status => ${2:401}\n\
+snippet rp\n\
+ render :partial => \"${1:item}\"\n\
+snippet rpc\n\
+ render :partial => \"${1:item}\", :collection => ${2:@$1s}\n\
+snippet rpl\n\
+ render :partial => \"${1:item}\", :locals => { :${2:$1} => ${3:@$1}\n\
+snippet rpo\n\
+ render :partial => \"${1:item}\", :object => ${2:@$1}\n\
+snippet rps\n\
+ render :partial => \"${1:item}\", :status => ${2:500}\n\
+snippet rt\n\
+ render :text => \"${1:text to render}\"\n\
+snippet rtl\n\
+ render :text => \"${1:text to render}\", :layout => \"${2:layoutname}\"\n\
+snippet rtlt\n\
+ render :text => \"${1:text to render}\", :layout => ${2:true}\n\
+snippet rts\n\
+ render :text => \"${1:text to render}\", :status => ${2:401}\n\
+snippet ru\n\
+ render :update do |${1:page}|\n\
+ $1.${2}\n\
+ end\n\
+snippet rxml\n\
+ render :xml => ${1:text to render}\n\
+snippet sc\n\
+ scope :${1:name}, :where(:@${2:field} => ${3:value})\n\
+snippet sl\n\
+ scope :${1:name}, lambda do |${2:value}|\n\
+ where(\"${3:field = ?}\", ${4:bind var})\n\
+ end\n\
+snippet sha1\n\
+ Digest::SHA1.hexdigest(${1:string})\n\
+snippet sweeper\n\
+ class ${1:ModelClassName}Sweeper < ActionController::Caching::Sweeper\n\
+ observe $1\n\
+\n\
+ def after_save(${2:model_class_name})\n\
+ expire_cache($2)\n\
+ end\n\
+\n\
+ def after_destroy($2)\n\
+ expire_cache($2)\n\
+ end\n\
+\n\
+ def expire_cache($2)\n\
+ expire_page\n\
+ end\n\
+ end\n\
+snippet tcb\n\
+ t.boolean :${1:title}\n\
+ ${2}\n\
+snippet tcbi\n\
+ t.binary :${1:title}, :limit => ${2:2}.megabytes\n\
+ ${3}\n\
+snippet tcd\n\
+ t.decimal :${1:title}, :precision => ${2:10}, :scale => ${3:2}\n\
+ ${4}\n\
+snippet tcda\n\
+ t.date :${1:title}\n\
+ ${2}\n\
+snippet tcdt\n\
+ t.datetime :${1:title}\n\
+ ${2}\n\
+snippet tcf\n\
+ t.float :${1:title}\n\
+ ${2}\n\
+snippet tch\n\
+ t.change :${1:name}, :${2:string}, :${3:limit} => ${4:80}\n\
+ ${5}\n\
+snippet tci\n\
+ t.integer :${1:title}\n\
+ ${2}\n\
+snippet tcl\n\
+ t.integer :lock_version, :null => false, :default => 0\n\
+ ${1}\n\
+snippet tcr\n\
+ t.references :${1:taggable}, :polymorphic => { :default => '${2:Photo}' }\n\
+ ${3}\n\
+snippet tcs\n\
+ t.string :${1:title}\n\
+ ${2}\n\
+snippet tct\n\
+ t.text :${1:title}\n\
+ ${2}\n\
+snippet tcti\n\
+ t.time :${1:title}\n\
+ ${2}\n\
+snippet tcts\n\
+ t.timestamp :${1:title}\n\
+ ${2}\n\
+snippet tctss\n\
+ t.timestamps\n\
+ ${1}\n\
+snippet va\n\
+ validates_associated :${1:attribute}\n\
+snippet vao\n\
+ validates_acceptance_of :${1:terms}\n\
+snippet vc\n\
+ validates_confirmation_of :${1:attribute}\n\
+snippet ve\n\
+ validates_exclusion_of :${1:attribute}, :in => ${2:%w( mov avi )}\n\
+snippet vf\n\
+ validates_format_of :${1:attribute}, :with => /${2:regex}/\n\
+snippet vi\n\
+ validates_inclusion_of :${1:attribute}, :in => %w(${2: mov avi })\n\
+snippet vl\n\
+ validates_length_of :${1:attribute}, :within => ${2:3}..${3:20}\n\
+snippet vn\n\
+ validates_numericality_of :${1:attribute}\n\
+snippet vpo\n\
+ validates_presence_of :${1:attribute}\n\
+snippet vu\n\
+ validates_uniqueness_of :${1:attribute}\n\
+snippet wants\n\
+ wants.${1:js|xml|html} { ${2} }\n\
+snippet wc\n\
+ where(${1:\"conditions\"}${2:, bind_var})\n\
+snippet wh\n\
+ where(${1:field} => ${2:value})\n\
+snippet xdelete\n\
+ xhr :delete, :${1:destroy}, :id => ${2:1}${3}\n\
+snippet xget\n\
+ xhr :get, :${1:show}, :id => ${2:1}${3}\n\
+snippet xpost\n\
+ xhr :post, :${1:create}, :${2:object} => { ${3} }\n\
+snippet xput\n\
+ xhr :put, :${1:update}, :id => ${2:1}, :${3:object} => { ${4} }${5}\n\
+snippet test\n\
+ test \"should ${1:do something}\" do\n\
+ ${2}\n\
+ end\n\
+#migrations\n\
+snippet mac\n\
+ add_column :${1:table_name}, :${2:column_name}, :${3:data_type}\n\
+snippet mrc\n\
+ remove_column :${1:table_name}, :${2:column_name}\n\
+snippet mrnc\n\
+ rename_column :${1:table_name}, :${2:old_column_name}, :${3:new_column_name}\n\
+snippet mcc\n\
+ change_column :${1:table}, :${2:column}, :${3:type}\n\
+snippet mccc\n\
+ t.column :${1:title}, :${2:string}\n\
+snippet mct\n\
+ create_table :${1:table_name} do |t|\n\
+ t.column :${2:name}, :${3:type}\n\
+ end\n\
+snippet migration\n\
+ class ${1:class_name} < ActiveRecord::Migration\n\
+ def self.up\n\
+ ${2}\n\
+ end\n\
+\n\
+ def self.down\n\
+ end\n\
+ end\n\
+\n\
+snippet trc\n\
+ t.remove :${1:column}\n\
+snippet tre\n\
+ t.rename :${1:old_column_name}, :${2:new_column_name}\n\
+ ${3}\n\
+snippet tref\n\
+ t.references :${1:model}\n\
+\n\
+#rspec\n\
+snippet it\n\
+ it \"${1:spec_name}\" do\n\
+ ${2}\n\
+ end\n\
+snippet itp\n\
+ it \"${1:spec_name}\"\n\
+ ${2}\n\
+snippet desc\n\
+ describe ${1:class_name} do\n\
+ ${2}\n\
+ end\n\
+snippet cont\n\
+ context \"${1:message}\" do\n\
+ ${2}\n\
+ end\n\
+snippet bef\n\
+ before :${1:each} do\n\
+ ${2}\n\
+ end\n\
+snippet aft\n\
+ after :${1:each} do\n\
+ ${2}\n\
+ end\n\
+";
+exports.scope = "ruby";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/rust.js b/services/web/public/js/ace-1.2.5/snippets/rust.js
new file mode 100644
index 0000000000..0411c63e15
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/rust.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/rust",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "rust";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/sass.js b/services/web/public/js/ace-1.2.5/snippets/sass.js
new file mode 100644
index 0000000000..b9adc9d8c3
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/sass.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/sass",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "sass";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/scad.js b/services/web/public/js/ace-1.2.5/snippets/scad.js
new file mode 100644
index 0000000000..998a98ac67
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/scad.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/scad",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "scad";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/scala.js b/services/web/public/js/ace-1.2.5/snippets/scala.js
new file mode 100644
index 0000000000..4051d98883
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/scala.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/scala",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "scala";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/scheme.js b/services/web/public/js/ace-1.2.5/snippets/scheme.js
new file mode 100644
index 0000000000..202d074150
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/scheme.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/scheme",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "scheme";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/scss.js b/services/web/public/js/ace-1.2.5/snippets/scss.js
new file mode 100644
index 0000000000..fbd98f74ca
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/scss.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/scss",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "scss";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/sh.js b/services/web/public/js/ace-1.2.5/snippets/sh.js
new file mode 100644
index 0000000000..12074a66ad
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/sh.js
@@ -0,0 +1,90 @@
+ace.define("ace/snippets/sh",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# Shebang. Executing bash via /usr/bin/env makes scripts more portable.\n\
+snippet #!\n\
+ #!/usr/bin/env bash\n\
+ \n\
+snippet if\n\
+ if [[ ${1:condition} ]]; then\n\
+ ${2:#statements}\n\
+ fi\n\
+snippet elif\n\
+ elif [[ ${1:condition} ]]; then\n\
+ ${2:#statements}\n\
+snippet for\n\
+ for (( ${2:i} = 0; $2 < ${1:count}; $2++ )); do\n\
+ ${3:#statements}\n\
+ done\n\
+snippet fori\n\
+ for ${1:needle} in ${2:haystack} ; do\n\
+ ${3:#statements}\n\
+ done\n\
+snippet wh\n\
+ while [[ ${1:condition} ]]; do\n\
+ ${2:#statements}\n\
+ done\n\
+snippet until\n\
+ until [[ ${1:condition} ]]; do\n\
+ ${2:#statements}\n\
+ done\n\
+snippet case\n\
+ case ${1:word} in\n\
+ ${2:pattern})\n\
+ ${3};;\n\
+ esac\n\
+snippet go \n\
+ while getopts '${1:o}' ${2:opts} \n\
+ do \n\
+ case $$2 in\n\
+ ${3:o0})\n\
+ ${4:#staments};;\n\
+ esac\n\
+ done\n\
+# Set SCRIPT_DIR variable to directory script is located.\n\
+snippet sdir\n\
+ SCRIPT_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\n\
+# getopt\n\
+snippet getopt\n\
+ __ScriptVersion=\"${1:version}\"\n\
+\n\
+ #=== FUNCTION ================================================================\n\
+ # NAME: usage\n\
+ # DESCRIPTION: Display usage information.\n\
+ #===============================================================================\n\
+ function usage ()\n\
+ {\n\
+ cat <<- EOT\n\
+\n\
+ Usage : $${0:0} [options] [--] \n\
+\n\
+ Options: \n\
+ -h|help Display this message\n\
+ -v|version Display script version\n\
+\n\
+ EOT\n\
+ } # ---------- end of function usage ----------\n\
+\n\
+ #-----------------------------------------------------------------------\n\
+ # Handle command line arguments\n\
+ #-----------------------------------------------------------------------\n\
+\n\
+ while getopts \":hv\" opt\n\
+ do\n\
+ case $opt in\n\
+\n\
+ h|help ) usage; exit 0 ;;\n\
+\n\
+ v|version ) echo \"$${0:0} -- Version $__ScriptVersion\"; exit 0 ;;\n\
+\n\
+ \\? ) echo -e \"\\n Option does not exist : $OPTARG\\n\"\n\
+ usage; exit 1 ;;\n\
+\n\
+ esac # --- end of case ---\n\
+ done\n\
+ shift $(($OPTIND-1))\n\
+\n\
+";
+exports.scope = "sh";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/sjs.js b/services/web/public/js/ace-1.2.5/snippets/sjs.js
new file mode 100644
index 0000000000..cf39a34ecd
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/sjs.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/sjs",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "sjs";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/smarty.js b/services/web/public/js/ace-1.2.5/snippets/smarty.js
new file mode 100644
index 0000000000..47319a2599
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/smarty.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/smarty",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "smarty";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/snippets.js b/services/web/public/js/ace-1.2.5/snippets/snippets.js
new file mode 100644
index 0000000000..b81605ccdf
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/snippets.js
@@ -0,0 +1,16 @@
+ace.define("ace/snippets/snippets",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# snippets for making snippets :)\n\
+snippet snip\n\
+ snippet ${1:trigger}\n\
+ ${2}\n\
+snippet msnip\n\
+ snippet ${1:trigger} ${2:description}\n\
+ ${3}\n\
+snippet v\n\
+ {VISUAL}\n\
+";
+exports.scope = "snippets";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/soy_template.js b/services/web/public/js/ace-1.2.5/snippets/soy_template.js
new file mode 100644
index 0000000000..908f5fdf65
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/soy_template.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/soy_template",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "soy_template";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/space.js b/services/web/public/js/ace-1.2.5/snippets/space.js
new file mode 100644
index 0000000000..302b84e00b
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/space.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/space",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "space";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/sql.js b/services/web/public/js/ace-1.2.5/snippets/sql.js
new file mode 100644
index 0000000000..1822126ba7
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/sql.js
@@ -0,0 +1,33 @@
+ace.define("ace/snippets/sql",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "snippet tbl\n\
+ create table ${1:table} (\n\
+ ${2:columns}\n\
+ );\n\
+snippet col\n\
+ ${1:name} ${2:type} ${3:default ''} ${4:not null}\n\
+snippet ccol\n\
+ ${1:name} varchar2(${2:size}) ${3:default ''} ${4:not null}\n\
+snippet ncol\n\
+ ${1:name} number ${3:default 0} ${4:not null}\n\
+snippet dcol\n\
+ ${1:name} date ${3:default sysdate} ${4:not null}\n\
+snippet ind\n\
+ create index ${3:$1_$2} on ${1:table}(${2:column});\n\
+snippet uind\n\
+ create unique index ${1:name} on ${2:table}(${3:column});\n\
+snippet tblcom\n\
+ comment on table ${1:table} is '${2:comment}';\n\
+snippet colcom\n\
+ comment on column ${1:table}.${2:column} is '${3:comment}';\n\
+snippet addcol\n\
+ alter table ${1:table} add (${2:column} ${3:type});\n\
+snippet seq\n\
+ create sequence ${1:name} start with ${2:1} increment by ${3:1} minvalue ${4:1};\n\
+snippet s*\n\
+ select * from ${1:table}\n\
+";
+exports.scope = "sql";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/sqlserver.js b/services/web/public/js/ace-1.2.5/snippets/sqlserver.js
new file mode 100644
index 0000000000..7dfa2d0493
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/sqlserver.js
@@ -0,0 +1,76 @@
+ace.define("ace/snippets/sqlserver",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# ISNULL\n\
+snippet isnull\n\
+ ISNULL(${1:check_expression}, ${2:replacement_value})\n\
+# FORMAT\n\
+snippet format\n\
+ FORMAT(${1:value}, ${2:format})\n\
+# CAST\n\
+snippet cast\n\
+ CAST(${1:expression} AS ${2:data_type})\n\
+# CONVERT\n\
+snippet convert\n\
+ CONVERT(${1:data_type}, ${2:expression})\n\
+# DATEPART\n\
+snippet datepart\n\
+ DATEPART(${1:datepart}, ${2:date})\n\
+# DATEDIFF\n\
+snippet datediff\n\
+ DATEDIFF(${1:datepart}, ${2:startdate}, ${3:enddate})\n\
+# DATEADD\n\
+snippet dateadd\n\
+ DATEADD(${1:datepart}, ${2:number}, ${3:date})\n\
+# DATEFROMPARTS \n\
+snippet datefromparts\n\
+ DATEFROMPARTS(${1:year}, ${2:month}, ${3:day})\n\
+# OBJECT_DEFINITION\n\
+snippet objectdef\n\
+ SELECT OBJECT_DEFINITION(OBJECT_ID('${1:sys.server_permissions /*object name*/}'))\n\
+# STUFF XML\n\
+snippet stuffxml\n\
+ STUFF((SELECT ', ' + ${1:ColumnName}\n\
+ FROM ${2:TableName}\n\
+ WHERE ${3:WhereClause}\n\
+ FOR XML PATH('')), 1, 1, '') AS ${4:Alias}\n\
+ ${5:/*https://msdn.microsoft.com/en-us/library/ms188043.aspx*/}\n\
+# Create Procedure\n\
+snippet createproc\n\
+ -- =============================================\n\
+ -- Author: ${1:Author}\n\
+ -- Create date: ${2:Date}\n\
+ -- Description: ${3:Description}\n\
+ -- =============================================\n\
+ CREATE PROCEDURE ${4:Procedure_Name}\n\
+ ${5:/*Add the parameters for the stored procedure here*/}\n\
+ AS\n\
+ BEGIN\n\
+ -- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.\n\
+ SET NOCOUNT ON;\n\
+ \n\
+ ${6:/*Add the T-SQL statements to compute the return value here*/}\n\
+ \n\
+ END\n\
+ GO\n\
+# Create Scalar Function\n\
+snippet createfn\n\
+ -- =============================================\n\
+ -- Author: ${1:Author}\n\
+ -- Create date: ${2:Date}\n\
+ -- Description: ${3:Description}\n\
+ -- =============================================\n\
+ CREATE FUNCTION ${4:Scalar_Function_Name}\n\
+ -- Add the parameters for the function here\n\
+ RETURNS ${5:Function_Data_Type}\n\
+ AS\n\
+ BEGIN\n\
+ DECLARE @Result ${5:Function_Data_Type}\n\
+ \n\
+ ${6:/*Add the T-SQL statements to compute the return value here*/}\n\
+ \n\
+ END\n\
+ GO";
+exports.scope = "sqlserver";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/stylus.js b/services/web/public/js/ace-1.2.5/snippets/stylus.js
new file mode 100644
index 0000000000..5f700bae33
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/stylus.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/stylus",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "stylus";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/svg.js b/services/web/public/js/ace-1.2.5/snippets/svg.js
new file mode 100644
index 0000000000..69a3408ec9
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/svg.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/svg",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "svg";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/swift.js b/services/web/public/js/ace-1.2.5/snippets/swift.js
new file mode 100644
index 0000000000..55226ba0cc
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/swift.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/swift",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "swift";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/tcl.js b/services/web/public/js/ace-1.2.5/snippets/tcl.js
new file mode 100644
index 0000000000..4d116da829
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/tcl.js
@@ -0,0 +1,99 @@
+ace.define("ace/snippets/tcl",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# #!/usr/bin/env tclsh\n\
+snippet #!\n\
+ #!/usr/bin/env tclsh\n\
+ \n\
+# Process\n\
+snippet pro\n\
+ proc ${1:function_name} {${2:args}} {\n\
+ ${3:#body ...}\n\
+ }\n\
+#xif\n\
+snippet xif\n\
+ ${1:expr}? ${2:true} : ${3:false}\n\
+# Conditional\n\
+snippet if\n\
+ if {${1}} {\n\
+ ${2:# body...}\n\
+ }\n\
+# Conditional if..else\n\
+snippet ife\n\
+ if {${1}} {\n\
+ ${2:# body...}\n\
+ } else {\n\
+ ${3:# else...}\n\
+ }\n\
+# Conditional if..elsif..else\n\
+snippet ifee\n\
+ if {${1}} {\n\
+ ${2:# body...}\n\
+ } elseif {${3}} {\n\
+ ${4:# elsif...}\n\
+ } else {\n\
+ ${5:# else...}\n\
+ }\n\
+# If catch then\n\
+snippet ifc\n\
+ if { [catch {${1:#do something...}} ${2:err}] } {\n\
+ ${3:# handle failure...}\n\
+ }\n\
+# Catch\n\
+snippet catch\n\
+ catch {${1}} ${2:err} ${3:options}\n\
+# While Loop\n\
+snippet wh\n\
+ while {${1}} {\n\
+ ${2:# body...}\n\
+ }\n\
+# For Loop\n\
+snippet for\n\
+ for {set ${2:var} 0} {$$2 < ${1:count}} {${3:incr} $2} {\n\
+ ${4:# body...}\n\
+ }\n\
+# Foreach Loop\n\
+snippet fore\n\
+ foreach ${1:x} {${2:#list}} {\n\
+ ${3:# body...}\n\
+ }\n\
+# after ms script...\n\
+snippet af\n\
+ after ${1:ms} ${2:#do something}\n\
+# after cancel id\n\
+snippet afc\n\
+ after cancel ${1:id or script}\n\
+# after idle\n\
+snippet afi\n\
+ after idle ${1:script}\n\
+# after info id\n\
+snippet afin\n\
+ after info ${1:id}\n\
+# Expr\n\
+snippet exp\n\
+ expr {${1:#expression here}}\n\
+# Switch\n\
+snippet sw\n\
+ switch ${1:var} {\n\
+ ${3:pattern 1} {\n\
+ ${4:#do something}\n\
+ }\n\
+ default {\n\
+ ${2:#do something}\n\
+ }\n\
+ }\n\
+# Case\n\
+snippet ca\n\
+ ${1:pattern} {\n\
+ ${2:#do something}\n\
+ }${3}\n\
+# Namespace eval\n\
+snippet ns\n\
+ namespace eval ${1:path} {${2:#script...}}\n\
+# Namespace current\n\
+snippet nsc\n\
+ namespace current\n\
+";
+exports.scope = "tcl";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/tex.js b/services/web/public/js/ace-1.2.5/snippets/tex.js
new file mode 100644
index 0000000000..2bd3f1034c
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/tex.js
@@ -0,0 +1,197 @@
+ace.define("ace/snippets/tex",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "#PREAMBLE\n\
+#newcommand\n\
+snippet nc\n\
+ \\newcommand{\\${1:cmd}}[${2:opt}]{${3:realcmd}}${4}\n\
+#usepackage\n\
+snippet up\n\
+ \\usepackage[${1:[options}]{${2:package}}\n\
+#newunicodechar\n\
+snippet nuc\n\
+ \\newunicodechar{${1}}{${2:\\ensuremath}${3:tex-substitute}}}\n\
+#DeclareMathOperator\n\
+snippet dmo\n\
+ \\DeclareMathOperator{${1}}{${2}}\n\
+\n\
+#DOCUMENT\n\
+# \\begin{}...\\end{}\n\
+snippet begin\n\
+ \\begin{${1:env}}\n\
+ ${2}\n\
+ \\end{$1}\n\
+# Tabular\n\
+snippet tab\n\
+ \\begin{${1:tabular}}{${2:c}}\n\
+ ${3}\n\
+ \\end{$1}\n\
+snippet thm\n\
+ \\begin[${1:author}]{${2:thm}}\n\
+ ${3}\n\
+ \\end{$1}\n\
+snippet center\n\
+ \\begin{center}\n\
+ ${1}\n\
+ \\end{center}\n\
+# Align(ed)\n\
+snippet ali\n\
+ \\begin{align${1:ed}}\n\
+ ${2}\n\
+ \\end{align$1}\n\
+# Gather(ed)\n\
+snippet gat\n\
+ \\begin{gather${1:ed}}\n\
+ ${2}\n\
+ \\end{gather$1}\n\
+# Equation\n\
+snippet eq\n\
+ \\begin{equation}\n\
+ ${1}\n\
+ \\end{equation}\n\
+# Equation\n\
+snippet eq*\n\
+ \\begin{equation*}\n\
+ ${1}\n\
+ \\end{equation*}\n\
+# Unnumbered Equation\n\
+snippet \\\n\
+ \\[\n\
+ ${1}\n\
+ \\]\n\
+# Enumerate\n\
+snippet enum\n\
+ \\begin{enumerate}\n\
+ \\item ${1}\n\
+ \\end{enumerate}\n\
+# Itemize\n\
+snippet itemize\n\
+ \\begin{itemize}\n\
+ \\item ${1}\n\
+ \\end{itemize}\n\
+# Description\n\
+snippet desc\n\
+ \\begin{description}\n\
+ \\item[${1}] ${2}\n\
+ \\end{description}\n\
+# Matrix\n\
+snippet mat\n\
+ \\begin{${1:p/b/v/V/B/small}matrix}\n\
+ ${2}\n\
+ \\end{$1matrix}\n\
+# Cases\n\
+snippet cas\n\
+ \\begin{cases}\n\
+ ${1:equation}, &\\text{ if }${2:case}\\\\\n\
+ ${3}\n\
+ \\end{cases}\n\
+# Split\n\
+snippet spl\n\
+ \\begin{split}\n\
+ ${1}\n\
+ \\end{split}\n\
+# Part\n\
+snippet part\n\
+ \\part{${1:part name}} % (fold)\n\
+ \\label{prt:${2:$1}}\n\
+ ${3}\n\
+ % part $2 (end)\n\
+# Chapter\n\
+snippet cha\n\
+ \\chapter{${1:chapter name}}\n\
+ \\label{cha:${2:$1}}\n\
+ ${3}\n\
+# Section\n\
+snippet sec\n\
+ \\section{${1:section name}}\n\
+ \\label{sec:${2:$1}}\n\
+ ${3}\n\
+# Sub Section\n\
+snippet sub\n\
+ \\subsection{${1:subsection name}}\n\
+ \\label{sub:${2:$1}}\n\
+ ${3}\n\
+# Sub Sub Section\n\
+snippet subs\n\
+ \\subsubsection{${1:subsubsection name}}\n\
+ \\label{ssub:${2:$1}}\n\
+ ${3}\n\
+# Paragraph\n\
+snippet par\n\
+ \\paragraph{${1:paragraph name}}\n\
+ \\label{par:${2:$1}}\n\
+ ${3}\n\
+# Sub Paragraph\n\
+snippet subp\n\
+ \\subparagraph{${1:subparagraph name}}\n\
+ \\label{subp:${2:$1}}\n\
+ ${3}\n\
+#References\n\
+snippet itd\n\
+ \\item[${1:description}] ${2:item}\n\
+snippet figure\n\
+ ${1:Figure}~\\ref{${2:fig:}}${3}\n\
+snippet table\n\
+ ${1:Table}~\\ref{${2:tab:}}${3}\n\
+snippet listing\n\
+ ${1:Listing}~\\ref{${2:list}}${3}\n\
+snippet section\n\
+ ${1:Section}~\\ref{${2:sec:}}${3}\n\
+snippet page\n\
+ ${1:page}~\\pageref{${2}}${3}\n\
+snippet index\n\
+ \\index{${1:index}}${2}\n\
+#Citations\n\
+snippet cite\n\
+ \\cite[${1}]{${2}}${3}\n\
+snippet fcite\n\
+ \\footcite[${1}]{${2}}${3}\n\
+#Formating text: italic, bold, underline, small capital, emphase ..\n\
+snippet it\n\
+ \\textit{${1:text}}\n\
+snippet bf\n\
+ \\textbf{${1:text}}\n\
+snippet under\n\
+ \\underline{${1:text}}\n\
+snippet emp\n\
+ \\emph{${1:text}}\n\
+snippet sc\n\
+ \\textsc{${1:text}}\n\
+#Choosing font\n\
+snippet sf\n\
+ \\textsf{${1:text}}\n\
+snippet rm\n\
+ \\textrm{${1:text}}\n\
+snippet tt\n\
+ \\texttt{${1:text}}\n\
+#misc\n\
+snippet ft\n\
+ \\footnote{${1:text}}\n\
+snippet fig\n\
+ \\begin{figure}\n\
+ \\begin{center}\n\
+ \\includegraphics[scale=${1}]{Figures/${2}}\n\
+ \\end{center}\n\
+ \\caption{${3}}\n\
+ \\label{fig:${4}}\n\
+ \\end{figure}\n\
+snippet tikz\n\
+ \\begin{figure}\n\
+ \\begin{center}\n\
+ \\begin{tikzpicture}[scale=${1:1}]\n\
+ ${2}\n\
+ \\end{tikzpicture}\n\
+ \\end{center}\n\
+ \\caption{${3}}\n\
+ \\label{fig:${4}}\n\
+ \\end{figure}\n\
+#math\n\
+snippet stackrel\n\
+ \\stackrel{${1:above}}{${2:below}} ${3}\n\
+snippet frac\n\
+ \\frac{${1:num}}{${2:denom}}\n\
+snippet sum\n\
+ \\sum^{${1:n}}_{${2:i=1}}{${3}}";
+exports.scope = "tex";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/text.js b/services/web/public/js/ace-1.2.5/snippets/text.js
new file mode 100644
index 0000000000..57b897bf67
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/text.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/text",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "text";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/textile.js b/services/web/public/js/ace-1.2.5/snippets/textile.js
new file mode 100644
index 0000000000..a6fd711efa
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/textile.js
@@ -0,0 +1,37 @@
+ace.define("ace/snippets/textile",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# Jekyll post header\n\
+snippet header\n\
+ ---\n\
+ title: ${1:title}\n\
+ layout: post\n\
+ date: ${2:date} ${3:hour:minute:second} -05:00\n\
+ ---\n\
+\n\
+# Image\n\
+snippet img\n\
+ !${1:url}(${2:title}):${3:link}!\n\
+\n\
+# Table\n\
+snippet |\n\
+ |${1}|${2}\n\
+\n\
+# Link\n\
+snippet link\n\
+ \"${1:link text}\":${2:url}\n\
+\n\
+# Acronym\n\
+snippet (\n\
+ (${1:Expand acronym})${2}\n\
+\n\
+# Footnote\n\
+snippet fn\n\
+ [${1:ref number}] ${3}\n\
+\n\
+ fn$1. ${2:footnote}\n\
+ \n\
+";
+exports.scope = "textile";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/toml.js b/services/web/public/js/ace-1.2.5/snippets/toml.js
new file mode 100644
index 0000000000..0c1a857bb1
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/toml.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/toml",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "toml";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/tsx.js b/services/web/public/js/ace-1.2.5/snippets/tsx.js
new file mode 100644
index 0000000000..7946297ead
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/tsx.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/tsx",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "tsx";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/twig.js b/services/web/public/js/ace-1.2.5/snippets/twig.js
new file mode 100644
index 0000000000..ccc6073cfd
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/twig.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/twig",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "twig";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/typescript.js b/services/web/public/js/ace-1.2.5/snippets/typescript.js
new file mode 100644
index 0000000000..5f6217d01b
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/typescript.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/typescript",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "typescript";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/vala.js b/services/web/public/js/ace-1.2.5/snippets/vala.js
new file mode 100644
index 0000000000..3b493422e7
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/vala.js
@@ -0,0 +1,193 @@
+ace.define("ace/snippets/vala",["require","exports","module"], function(require, exports, module) {
+"use strict";
+exports.snippets = [
+ {
+ "content": "case ${1:condition}:\n\t$0\n\tbreak;\n",
+ "name": "case",
+ "scope": "vala",
+ "tabTrigger": "case"
+ },
+ {
+ "content": "/**\n * ${6}\n */\n${1:public} class ${2:MethodName}${3: : GLib.Object} {\n\n\t/**\n\t * ${7}\n\t */\n\tpublic ${2}(${4}) {\n\t\t${5}\n\t}\n\n\t$0\n}",
+ "name": "class",
+ "scope": "vala",
+ "tabTrigger": "class"
+ },
+ {
+ "content": "(${1}) => {\n\t${0}\n}\n",
+ "name": "closure",
+ "scope": "vala",
+ "tabTrigger": "=>"
+ },
+ {
+ "content": "/*\n * $0\n */",
+ "name": "Comment (multiline)",
+ "scope": "vala",
+ "tabTrigger": "/*"
+ },
+ {
+ "content": "Console.WriteLine($1);\n$0",
+ "name": "Console.WriteLine (writeline)",
+ "scope": "vala",
+ "tabTrigger": "writeline"
+ },
+ {
+ "content": "[DBus(name = \"$0\")]",
+ "name": "DBus annotation",
+ "scope": "vala",
+ "tabTrigger": "[DBus"
+ },
+ {
+ "content": "delegate ${1:void} ${2:DelegateName}($0);",
+ "name": "delegate",
+ "scope": "vala",
+ "tabTrigger": "delegate"
+ },
+ {
+ "content": "do {\n\t$0\n} while ($1);\n",
+ "name": "do while",
+ "scope": "vala",
+ "tabTrigger": "dowhile"
+ },
+ {
+ "content": "/**\n * $0\n */",
+ "name": "DocBlock",
+ "scope": "vala",
+ "tabTrigger": "/**"
+ },
+ {
+ "content": "else if ($1) {\n\t$0\n}\n",
+ "name": "else if (elseif)",
+ "scope": "vala",
+ "tabTrigger": "elseif"
+ },
+ {
+ "content": "else {\n\t$0\n}",
+ "name": "else",
+ "scope": "vala",
+ "tabTrigger": "else"
+ },
+ {
+ "content": "enum {$1:EnumName} {\n\t$0\n}",
+ "name": "enum",
+ "scope": "vala",
+ "tabTrigger": "enum"
+ },
+ {
+ "content": "public errordomain ${1:Error} {\n\t$0\n}",
+ "name": "error domain",
+ "scope": "vala",
+ "tabTrigger": "errordomain"
+ },
+ {
+ "content": "for ($1;$2;$3) {\n\t$0\n}",
+ "name": "for",
+ "scope": "vala",
+ "tabTrigger": "for"
+ },
+ {
+ "content": "foreach ($1 in $2) {\n\t$0\n}",
+ "name": "foreach",
+ "scope": "vala",
+ "tabTrigger": "foreach"
+ },
+ {
+ "content": "Gee.ArrayList<${1:G}>($0);",
+ "name": "Gee.ArrayList",
+ "scope": "vala",
+ "tabTrigger": "ArrayList"
+ },
+ {
+ "content": "Gee.HashMap<${1:K},${2:V}>($0);",
+ "name": "Gee.HashMap",
+ "scope": "vala",
+ "tabTrigger": "HashMap"
+ },
+ {
+ "content": "Gee.HashSet<${1:G}>($0);",
+ "name": "Gee.HashSet",
+ "scope": "vala",
+ "tabTrigger": "HashSet"
+ },
+ {
+ "content": "if ($1) {\n\t$0\n}",
+ "name": "if",
+ "scope": "vala",
+ "tabTrigger": "if"
+ },
+ {
+ "content": "interface ${1:InterfaceName}{$2: : SuperInterface} {\n\t$0\n}",
+ "name": "interface",
+ "scope": "vala",
+ "tabTrigger": "interface"
+ },
+ {
+ "content": "public static int main(string [] argv) {\n\t${0}\n\treturn 0;\n}",
+ "name": "Main function",
+ "scope": "vala",
+ "tabTrigger": "main"
+ },
+ {
+ "content": "namespace $1 {\n\t$0\n}\n",
+ "name": "namespace (ns)",
+ "scope": "vala",
+ "tabTrigger": "ns"
+ },
+ {
+ "content": "stdout.printf($0);",
+ "name": "printf",
+ "scope": "vala",
+ "tabTrigger": "printf"
+ },
+ {
+ "content": "${1:public} ${2:Type} ${3:Name} {\n\tset {\n\t\t$0\n\t}\n\tget {\n\n\t}\n}",
+ "name": "property (prop)",
+ "scope": "vala",
+ "tabTrigger": "prop"
+ },
+ {
+ "content": "${1:public} ${2:Type} ${3:Name} {\n\tget {\n\t\t$0\n\t}\n}",
+ "name": "read-only property (roprop)",
+ "scope": "vala",
+ "tabTrigger": "roprop"
+ },
+ {
+ "content": "@\"${1:\\$var}\"",
+ "name": "String template (@)",
+ "scope": "vala",
+ "tabTrigger": "@"
+ },
+ {
+ "content": "struct ${1:StructName} {\n\t$0\n}",
+ "name": "struct",
+ "scope": "vala",
+ "tabTrigger": "struct"
+ },
+ {
+ "content": "switch ($1) {\n\t$0\n}",
+ "name": "switch",
+ "scope": "vala",
+ "tabTrigger": "switch"
+ },
+ {
+ "content": "try {\n\t$2\n} catch (${1:Error} e) {\n\t$0\n}",
+ "name": "try/catch",
+ "scope": "vala",
+ "tabTrigger": "try"
+ },
+ {
+ "content": "\"\"\"$0\"\"\";",
+ "name": "Verbatim string (\"\"\")",
+ "scope": "vala",
+ "tabTrigger": "verbatim"
+ },
+ {
+ "content": "while ($1) {\n\t$0\n}",
+ "name": "while",
+ "scope": "vala",
+ "tabTrigger": "while"
+ }
+];
+exports.scope = "";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/vbscript.js b/services/web/public/js/ace-1.2.5/snippets/vbscript.js
new file mode 100644
index 0000000000..38ca68fb2c
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/vbscript.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/vbscript",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "vbscript";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/velocity.js b/services/web/public/js/ace-1.2.5/snippets/velocity.js
new file mode 100644
index 0000000000..e2b12a45e3
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/velocity.js
@@ -0,0 +1,36 @@
+ace.define("ace/snippets/velocity",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "# macro\n\
+snippet #macro\n\
+ #macro ( ${1:macroName} ${2:\\$var1, [\\$var2, ...]} )\n\
+ ${3:## macro code}\n\
+ #end\n\
+# foreach\n\
+snippet #foreach\n\
+ #foreach ( ${1:\\$item} in ${2:\\$collection} )\n\
+ ${3:## foreach code}\n\
+ #end\n\
+# if\n\
+snippet #if\n\
+ #if ( ${1:true} )\n\
+ ${0}\n\
+ #end\n\
+# if ... else\n\
+snippet #ife\n\
+ #if ( ${1:true} )\n\
+ ${2}\n\
+ #else\n\
+ ${0}\n\
+ #end\n\
+#import\n\
+snippet #import\n\
+ #import ( \"${1:path/to/velocity/format}\" )\n\
+# set\n\
+snippet #set\n\
+ #set ( $${1:var} = ${0} )\n\
+";
+exports.scope = "velocity";
+exports.includeScopes = ["html", "javascript", "css"];
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/verilog.js b/services/web/public/js/ace-1.2.5/snippets/verilog.js
new file mode 100644
index 0000000000..8103ff6f26
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/verilog.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/verilog",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "verilog";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/vhdl.js b/services/web/public/js/ace-1.2.5/snippets/vhdl.js
new file mode 100644
index 0000000000..10d8ca09ce
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/vhdl.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/vhdl",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "vhdl";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/wollok.js b/services/web/public/js/ace-1.2.5/snippets/wollok.js
new file mode 100644
index 0000000000..31e62118d2
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/wollok.js
@@ -0,0 +1,91 @@
+ace.define("ace/snippets/wollok",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "##\n\
+## Basic Java packages and import\n\
+snippet im\n\
+ import\n\
+snippet w.l\n\
+ wollok.lang\n\
+snippet w.i\n\
+ wollok.lib\n\
+\n\
+## Class and object\n\
+snippet cl\n\
+ class ${1:`Filename(\"\", \"untitled\")`} ${2}\n\
+snippet obj\n\
+ object ${1:`Filename(\"\", \"untitled\")`} ${2:inherits Parent}${3}\n\
+snippet te\n\
+ test ${1:`Filename(\"\", \"untitled\")`}\n\
+\n\
+##\n\
+## Enhancements\n\
+snippet inh\n\
+ inherits\n\
+\n\
+##\n\
+## Comments\n\
+snippet /*\n\
+ /*\n\
+ * ${1}\n\
+ */\n\
+\n\
+##\n\
+## Control Statements\n\
+snippet el\n\
+ else\n\
+snippet if\n\
+ if (${1}) ${2}\n\
+\n\
+##\n\
+## Create a Method\n\
+snippet m\n\
+ method ${1:method}(${2}) ${5}\n\
+\n\
+## \n\
+## Tests\n\
+snippet as\n\
+ assert.equals(${1:expected}, ${2:actual})\n\
+\n\
+##\n\
+## Exceptions\n\
+snippet ca\n\
+ catch ${1:e} : (${2:Exception} ) ${3}\n\
+snippet thr\n\
+ throw\n\
+snippet try\n\
+ try {\n\
+ ${3}\n\
+ } catch ${1:e} : ${2:Exception} {\n\
+ }\n\
+\n\
+##\n\
+## Javadocs\n\
+snippet /**\n\
+ /**\n\
+ * ${1}\n\
+ */\n\
+\n\
+##\n\
+## Print Methods\n\
+snippet print\n\
+ console.println(\"${1:Message}\")\n\
+\n\
+##\n\
+## Setter and Getter Methods\n\
+snippet set\n\
+ method set${1:}(${2:}) {\n\
+ $1 = $2\n\
+ }\n\
+snippet get\n\
+ method get${1:}() {\n\
+ return ${1:};\n\
+ }\n\
+\n\
+##\n\
+## Terminate Methods or Loops\n\
+snippet re\n\
+ return";
+exports.scope = "wollok";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/xml.js b/services/web/public/js/ace-1.2.5/snippets/xml.js
new file mode 100644
index 0000000000..ee4b688a7c
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/xml.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/xml",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "xml";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/xquery.js b/services/web/public/js/ace-1.2.5/snippets/xquery.js
new file mode 100644
index 0000000000..c880abcf17
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/xquery.js
@@ -0,0 +1,68 @@
+ace.define("ace/snippets/xquery",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText = "snippet for\n\
+ for $${1:item} in ${2:expr}\n\
+snippet return\n\
+ return ${1:expr}\n\
+snippet import\n\
+ import module namespace ${1:ns} = \"${2:http://www.example.com/}\";\n\
+snippet some\n\
+ some $${1:varname} in ${2:expr} satisfies ${3:expr}\n\
+snippet every\n\
+ every $${1:varname} in ${2:expr} satisfies ${3:expr}\n\
+snippet if\n\
+ if(${1:true}) then ${2:expr} else ${3:true}\n\
+snippet switch\n\
+ switch(${1:\"foo\"})\n\
+ case ${2:\"foo\"}\n\
+ return ${3:true}\n\
+ default return ${4:false}\n\
+snippet try\n\
+ try { ${1:expr} } catch ${2:*} { ${3:expr} }\n\
+snippet tumbling\n\
+ for tumbling window $${1:varname} in ${2:expr}\n\
+ start at $${3:start} when ${4:expr}\n\
+ end at $${5:end} when ${6:expr}\n\
+ return ${7:expr}\n\
+snippet sliding\n\
+ for sliding window $${1:varname} in ${2:expr}\n\
+ start at $${3:start} when ${4:expr}\n\
+ end at $${5:end} when ${6:expr}\n\
+ return ${7:expr}\n\
+snippet let\n\
+ let $${1:varname} := ${2:expr}\n\
+snippet group\n\
+ group by $${1:varname} := ${2:expr}\n\
+snippet order\n\
+ order by ${1:expr} ${2:descending}\n\
+snippet stable\n\
+ stable order by ${1:expr}\n\
+snippet count\n\
+ count $${1:varname}\n\
+snippet ordered\n\
+ ordered { ${1:expr} }\n\
+snippet unordered\n\
+ unordered { ${1:expr} }\n\
+snippet treat \n\
+ treat as ${1:expr}\n\
+snippet castable\n\
+ castable as ${1:atomicType}\n\
+snippet cast\n\
+ cast as ${1:atomicType}\n\
+snippet typeswitch\n\
+ typeswitch(${1:expr})\n\
+ case ${2:type} return ${3:expr}\n\
+ default return ${4:expr}\n\
+snippet var\n\
+ declare variable $${1:varname} := ${2:expr};\n\
+snippet fn\n\
+ declare function ${1:ns}:${2:name}(){\n\
+ ${3:expr}\n\
+ };\n\
+snippet module\n\
+ module namespace ${1:ns} = \"${2:http://www.example.com}\";\n\
+";
+exports.scope = "xquery";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/snippets/yaml.js b/services/web/public/js/ace-1.2.5/snippets/yaml.js
new file mode 100644
index 0000000000..1adceabee1
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/snippets/yaml.js
@@ -0,0 +1,7 @@
+ace.define("ace/snippets/yaml",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.snippetText =undefined;
+exports.scope = "yaml";
+
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-ambiance.js b/services/web/public/js/ace-1.2.5/theme-ambiance.js
new file mode 100644
index 0000000000..1e53ecd969
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-ambiance.js
@@ -0,0 +1,182 @@
+ace.define("ace/theme/ambiance",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-ambiance";
+exports.cssText = ".ace-ambiance .ace_gutter {\
+background-color: #3d3d3d;\
+background-image: -moz-linear-gradient(left, #3D3D3D, #333);\
+background-image: -ms-linear-gradient(left, #3D3D3D, #333);\
+background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#3D3D3D), to(#333));\
+background-image: -webkit-linear-gradient(left, #3D3D3D, #333);\
+background-image: -o-linear-gradient(left, #3D3D3D, #333);\
+background-image: linear-gradient(left, #3D3D3D, #333);\
+background-repeat: repeat-x;\
+border-right: 1px solid #4d4d4d;\
+text-shadow: 0px 1px 1px #4d4d4d;\
+color: #222;\
+}\
+.ace-ambiance .ace_gutter-layer {\
+background: repeat left top;\
+}\
+.ace-ambiance .ace_gutter-active-line {\
+background-color: #3F3F3F;\
+}\
+.ace-ambiance .ace_fold-widget {\
+text-align: center;\
+}\
+.ace-ambiance .ace_fold-widget:hover {\
+color: #777;\
+}\
+.ace-ambiance .ace_fold-widget.ace_start,\
+.ace-ambiance .ace_fold-widget.ace_end,\
+.ace-ambiance .ace_fold-widget.ace_closed{\
+background: none;\
+border: none;\
+box-shadow: none;\
+}\
+.ace-ambiance .ace_fold-widget.ace_start:after {\
+content: '▾'\
+}\
+.ace-ambiance .ace_fold-widget.ace_end:after {\
+content: '▴'\
+}\
+.ace-ambiance .ace_fold-widget.ace_closed:after {\
+content: '‣'\
+}\
+.ace-ambiance .ace_print-margin {\
+border-left: 1px dotted #2D2D2D;\
+right: 0;\
+background: #262626;\
+}\
+.ace-ambiance .ace_scroller {\
+-webkit-box-shadow: inset 0 0 10px black;\
+-moz-box-shadow: inset 0 0 10px black;\
+-o-box-shadow: inset 0 0 10px black;\
+box-shadow: inset 0 0 10px black;\
+}\
+.ace-ambiance {\
+color: #E6E1DC;\
+background-color: #202020;\
+}\
+.ace-ambiance .ace_cursor {\
+border-left: 1px solid #7991E8;\
+}\
+.ace-ambiance .ace_overwrite-cursors .ace_cursor {\
+border: 1px solid #FFE300;\
+background: #766B13;\
+}\
+.ace-ambiance.normal-mode .ace_cursor-layer {\
+z-index: 0;\
+}\
+.ace-ambiance .ace_marker-layer .ace_selection {\
+background: rgba(221, 240, 255, 0.20);\
+}\
+.ace-ambiance .ace_marker-layer .ace_selected-word {\
+border-radius: 4px;\
+border: 8px solid #3f475d;\
+box-shadow: 0 0 4px black;\
+}\
+.ace-ambiance .ace_marker-layer .ace_step {\
+background: rgb(198, 219, 174);\
+}\
+.ace-ambiance .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(255, 255, 255, 0.25);\
+}\
+.ace-ambiance .ace_marker-layer .ace_active-line {\
+background: rgba(255, 255, 255, 0.031);\
+}\
+.ace-ambiance .ace_invisible {\
+color: #333;\
+}\
+.ace-ambiance .ace_paren {\
+color: #24C2C7;\
+}\
+.ace-ambiance .ace_keyword {\
+color: #cda869;\
+}\
+.ace-ambiance .ace_keyword.ace_operator {\
+color: #fa8d6a;\
+}\
+.ace-ambiance .ace_punctuation.ace_operator {\
+color: #fa8d6a;\
+}\
+.ace-ambiance .ace_identifier {\
+}\
+.ace-ambiance .ace-statement {\
+color: #cda869;\
+}\
+.ace-ambiance .ace_constant {\
+color: #CF7EA9;\
+}\
+.ace-ambiance .ace_constant.ace_language {\
+color: #CF7EA9;\
+}\
+.ace-ambiance .ace_constant.ace_library {\
+}\
+.ace-ambiance .ace_constant.ace_numeric {\
+color: #78CF8A;\
+}\
+.ace-ambiance .ace_invalid {\
+text-decoration: underline;\
+}\
+.ace-ambiance .ace_invalid.ace_illegal {\
+color:#F8F8F8;\
+background-color: rgba(86, 45, 86, 0.75);\
+}\
+.ace-ambiance .ace_invalid,\
+.ace-ambiance .ace_deprecated {\
+text-decoration: underline;\
+font-style: italic;\
+color: #D2A8A1;\
+}\
+.ace-ambiance .ace_support {\
+color: #9B859D;\
+}\
+.ace-ambiance .ace_support.ace_function {\
+color: #DAD085;\
+}\
+.ace-ambiance .ace_function.ace_buildin {\
+color: #9b859d;\
+}\
+.ace-ambiance .ace_string {\
+color: #8f9d6a;\
+}\
+.ace-ambiance .ace_string.ace_regexp {\
+color: #DAD085;\
+}\
+.ace-ambiance .ace_comment {\
+font-style: italic;\
+color: #555;\
+}\
+.ace-ambiance .ace_comment.ace_doc {\
+}\
+.ace-ambiance .ace_comment.ace_doc.ace_tag {\
+color: #666;\
+font-style: normal;\
+}\
+.ace-ambiance .ace_definition,\
+.ace-ambiance .ace_type {\
+color: #aac6e3;\
+}\
+.ace-ambiance .ace_variable {\
+color: #9999cc;\
+}\
+.ace-ambiance .ace_variable.ace_language {\
+color: #9b859d;\
+}\
+.ace-ambiance .ace_xml-pe {\
+color: #494949;\
+}\
+.ace-ambiance .ace_gutter-layer,\
+.ace-ambiance .ace_text-layer {\
+background-image: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAQAAAAHUWYVAABFFUlEQVQYGbzBCeDVU/74/6fj9HIcx/FRHx9JCFmzMyGRURhLZIkUsoeRfUjS2FNDtr6WkMhO9sm+S8maJfu+Jcsg+/o/c+Z4z/t97/vezy3z+z8ekGlnYICG/o7gdk+wmSHZ1z4pJItqapjoKXWahm8NmV6eOTbWUOp6/6a/XIg6GQqmenJ2lDHyvCFZ2cBDbmtHA043VFhHwXxClWmeYAdLhV00Bd85go8VmaFCkbVkzlQENzfBDZ5gtN7HwF0KDrTwJ0dypSOzpaKCMwQHKTIreYIxlmhXTzTWkVm+LTynZhiSBT3RZQ7aGfjGEd3qyXQ1FDymqbKxpspERQN2MiRjNZlFFQXfCNFm9nM1zpAsoYjmtRTc5ajwuaXc5xrWskT97RaKzAGe5ARHhVUsDbjKklziiX5WROcJwSNCNI+9w1Jwv4Zb2r7lCMZ4oq5C0EdTx+2GzNuKpJ+iFf38JEWkHJn9DNF7mmBDITrWEg0VWL3pHU20tSZnuqWu+R3BtYa8XxV1HO7GyD32UkOpL/yDloINFTmvtId+nmAjxRw40VMwVKiwrKLE4bK5UOVntYwhOcSSXKrJHKPJedocpGjVz/ZMIbnYUPB10/eKCrs5apqpgVmWzBYWpmtKHecJPjaUuEgRDDaU0oZghCJ6zNMQ5ZhDYx05r5v2muQdM0EILtXUsaKiQX9WMEUotagQzFbUNN6NUPC2nm5pxEWGCjMc3GdJHjSU2kORLK/JGSrkfGEIjncU/CYUnOipoYemwj8tST9NsJmB7TUVXtbUtXATJVZXBMvYeTXJfobgJUPmGMP/yFaWonaa6BcFO3nqcIqCozSZoZoSr1g4zJOzuyGnxTEX3lUEJ7WcZgme8ddaWvWJo2AJR9DZU3CUIbhCSG6ybSwN6qtJVnCU2svDTP2ZInOw2cBTrqtQahtNZn9NcJ4l2NaSmSkkP1noZWnVwkLmdUPOwLZEwy2Z3S3R+4rIG9hcbpPXHFVWcQdZkn2FOta3cKWQnNRC5g1LsJah4GCzSVsKnCOY5OAFRTBekyyryeyilhFKva75r4Mc0aWanGEaThcy31s439KKxTzJYY5WTHPU1FtIHjQU3Oip4xlNzj/lBw23dYZVliQa7WAXf4shetcQfatI+jWRDBPmyNeW6A1P5kdDgyYJlba0BIM8BZu1JfrFwItyjcAMR3K0BWOIrtMEXyhyrlVEx3ui5dUBjmB/Q3CXW85R4mBD0s7B+4q5tKUjOlb9qqmhi5AZ6GFIC5HXtOobdYGlVdMVbNJ8toNTFcHxnoL+muBagcctjWnbNMuR00uI7nQESwg5q2qqrKWIfrNUmeQocY6HuyxJV02wj36w00yhpmUFenv4p6fUkZYqLyuinx2RGOjhCXYyJF84oiU00YMOOhhquNdfbOB7gU88pY4xJO8LVdp6/q2voeB4R04vIdhSE40xZObx1HGGJ/ja0LBthFInKaLPPFzuCaYaoj8JjPME8yoyxo6zlBqkiUZYgq00OYMswbWO5NGmq+xhipxHLRW29ARjNKXO0wRnear8XSg4XFPLKEPUS1GqvyLwiuBUoa7zpZ0l5xxFwWmWZC1H5h5FwU8eQ7K+g8UcVY6TMQreVQT/8uQ8Z+ALIXnSEa2pYZQneE9RZbSBNYXfWYJzW/h/4j4Dp1tYVcFIC5019Vyi4ThPqSFCzjGWaHQTBU8q6vrVwgxP9Lkm840imWKpcLCjYTtrKuwvsKSnrvHCXGkSMk9p6lhckfRpIeis+N2PiszT+mFLspyGleUhDwcLrZqmyeylxwjBcKHEapqkmyangyLZRVOijwOtCY5SsG5zL0OwlCJ4y5KznF3EUNDDrinwiyLZRzOXtlBbK5ITHFGLp8Q0R6ab6mS7enI2cFrxOyHvOCFaT1HThS1krjCwqWeurCkk+willhCC+RSZnRXBiZaC5RXRIZYKp2lyfrHwiKPKR0JDzrdU2EFgpidawlFDR6FgXUMNa+g1FY3bUQh2cLCwosRdnuQTS/S+JVrGLeWIvtQUvONJxlqSQYYKpwoN2kaocLjdVsis4Mk80ESF2YpSkzwldjHkjFCUutI/r+EHDU8oCs6yzL3PhWiEooZdFMkymlas4AcI3KmoMMNSQ3tHzjGWCrcJJdYyZC7QFGwjRL9p+MrRkAGWzIaWCn9W0F3TsK01c2ZvQw0byvxuQU0r1lM0qJO7wW0kRIMdDTtXEdzi4VIh+EoIHm0mWtAtpCixlabgn83fKTI7anJe9ST7WIK1DMGpQmYeA58ImV6ezOGOzK2Kgq01pd60cKWiUi9Lievb/0vIDPHQ05Kzt4ddPckQBQtoaurjyHnek/nKzpQLrVgKPjIkh2v4uyezpv+Xoo7fPFXaGFp1vaLKxQ4uUpQQS5VuQs7BCq4xRJv7fwpVvvFEB3j+620haOuocqMhWd6TTPAEx+mdFNGHdranFe95WrWmIvlY4F1Dle2ECgc6cto7SryuqGGGha0tFQ5V53migUKmg6XKAo4qS3mik+0OZpAhOLeZKicacgaYcyx5hypYQE02ZA4xi/pNhOQxR4klNKyqacj+mpxnLTnnGSo85++3ZCZq6lrZkXlGEX3o+C9FieccJbZWVFjC0Yo1FZnJhoYMFoI1hEZ9r6hwg75HwzBNhbZCdJEfJwTPGzJvaKImw1yYX1HDAmpXR+ZJQ/SmgqMNVQb5vgamGwLtt7VwvP7Qk1xpiM5x5Cyv93E06MZmgs0Nya2azIKOYKCGBQQW97RmhKNKF02JZqHEJ4o58qp7X5EcZmc56trXEqzjCBZ1MFGR87Ql2tSTs6CGxS05PTzRQorkbw7aKoKXFDXsYW42VJih/q+FP2BdTzDTwVqOYB13liM50vG7wy28qagyuIXMeQI/Oqq8bcn5wJI50xH00CRntyfpL1T4hydYpoXgNiFzoIUTDZnLNRzh4TBHwbYGDvZkxmlyJloyr6tRihpeUG94GnKtIznREF0tzJG/OOr73JBcrSh1k6WuTprgLU+mnSGnv6Zge0NNz+kTDdH8nuAuTdJDCNb21LCiIuqlYbqGzT3RAoZofQfjFazkqeNWdYaGvYTM001EW2oKPvVk1ldUGSgUtHFwjKM1h9jnFcmy5lChoLNaQMGGDsYbKixlaMBmmsx1QjCfflwTfO/gckW0ruZ3jugKR3R5W9hGUWqCgxuFgsuaCHorotGKzGaeZB9DMsaTnKCpMtwTvOzhYk0rdrArKCqcaWmVk1+F372ur1YkKxgatI8Qfe1gIX9wE9FgS8ESmuABIXnRUbCapcKe+nO7slClSZFzpV/LkLncEb1qiO42fS3R855Su2mCLh62t1SYZZYVmKwIHjREF2uihTzB20JOkz7dkxzYQnK0UOU494wh+VWRc6Un2kpTaVgLDFEkJ/uhzRcI0YKGgpGWOlocBU/a4fKoJ/pEaNV6jip3+Es9VXY078rGnmAdf7t9ylPXS34RBSuYPs1UecZTU78WanhBCHpZ5sAoTz0LGZKjPf9TRypqWEiTvOFglL1fCEY3wY/++rbk7C8bWebA6p6om6PgOL2kp44TFJlVNBXae2rqqdZztOJpT87GQsE9jqCPIe9VReZuQ/CIgacsyZdCpIScSYqcZk8r+nsyCzhyfhOqHGOIvrLknC8wTpFcaYiGC/RU1NRbUeUpocQOnkRpGOrIOcNRx+1uA0UrzhSSt+VyS3SJpnFWkzNDqOFGIWcfR86DnmARTQ1HKIL33ExPiemeOhYSSjzlSUZZuE4TveoJLnBUOFof6KiysCbnAEcZgcUNTDOwkqWu3RWtmGpZwlHhJENdZ3miGz0lJlsKnjbwqSHQjpxnFDlTLLwqJPMZMjd7KrzkSG7VsxXBZE+F8YZkb01Oe00yyRK9psh5SYh29ySPKBo2ylNht7ZkZnsKenjKNJu9PNEyZpaCHv4Kt6RQsLvAVp7M9kIimmCUwGeWqLMmGuIotYMmWNpSahkhZw9FqZsVnKJhsjAHvtHMsTM9fCI06Dx/u3vfUXCqfsKRc4oFY2jMsoo/7DJDwZ1CsIKnJu+J9ldkpmiCxQx1rWjI+T9FwcWWzOuaYH0Hj7klNRVWEQpmaqosakiGNTFHdjS/qnUdmf0NJW5xsL0HhimCCZZSRzmSPTXJQ4aaztAwtZnoabebJ+htCaZ7Cm535ByoqXKbX1WRc4Eh2MkRXWzImVc96Cj4VdOKVxR84VdQsIUM8Psoou2byVHyZFuq7O8otbSQ2UAoeEWTudATLGSpZzVLlXVkPU2Jc+27lsw2jmg5T5VhbeE3BT083K9WsTTkFU/Osi0rC5lRlpwRHUiesNS0sOvmqGML1aRbPAxTJD9ZKtxuob+hhl8cwYGWpJ8nub7t5p6coYbMovZ1BTdaKn1jYD6h4GFDNFyT/Kqe1XCXphXHOKLZmuRSRdBPEfVUXQzJm5YGPGGJdvAEr7hHNdGZnuBvrpciGmopOLf5N0uVMy0FfYToJk90uUCbJupaVpO53UJXR2bVpoU00V2KOo4zMFrBd0Jtz2pa0clT5Q5L8IpQ177mWQejPMEJhuQjS10ref6HHjdEhy1P1EYR7GtO0uSsKJQYLiTnG1rVScj5lyazpqWGl5uBbRWl7m6ixGOOnEsMJR7z8J0n6KMnCdxhiNYQCoZ6CmYLnO8omC3MkW3bktlPmEt/VQQHejL3+dOE5FlPdK/Mq8hZxxJtLyRrepLThYKbLZxkSb5W52vYxNOaOxUF0yxMUPwBTYqCzy01XayYK0sJyWBLqX0MwU5CzoymRzV0EjjeUeLgDpTo6ij42ZAzvD01dHUUTPLU96MdLbBME8nFBn7zJCMtJcZokn8YoqU0FS5WFKyniHobguMcmW8N0XkWZjkyN3hqOMtS08r+/xTBwpZSZ3qiVRX8SzMHHjfUNFjgHEPmY9PL3ykEzxkSre/1ZD6z/NuznuB0RcE1TWTm9zRgfUWVJiG6yrzgmWPXC8EAR4Wxhlad0ZbgQyEz3pG5RVEwwDJH2mgKpjcTiCOzn1lfUWANFbZ2BA8balnEweJC9J0iuaeZoI+ippFCztEKVvckR2iice1JvhVytrQwUAZpgsubCPaU7xUe9vWnaOpaSBEspalykhC9bUlOMpT42ZHca6hyrqKmw/wMR8H5ZmdFoBVJb03O4UL0tSNnvIeRmkrLWqrs78gcrEn2tpcboh0UPOW3UUR9PMk4T4nnNKWmCjlrefhCwxRNztfmIQVdDElvS4m1/WuOujoZCs5XVOjtKPGokJzsYCtFYoWonSPT21DheU/wWhM19FcElwqNGOsp9Q8N/cwXaiND1MmeL1Q5XROtYYgGeFq1aTMsoMmcrKjQrOFQTQ1fmBYhmW6o8Jkjc7iDJRTBIo5kgJD5yMEYA3srCg7VFKwiVJkmRCc5ohGOKhsYMn/XBLdo5taZjlb9YAlGWRimqbCsoY7HFAXLa5I1HPRxMMsQDHFkWtRNniqT9UEeNjcE7RUlrCJ4R2CSJuqlKHWvJXjAUNcITYkenuBRB84TbeepcqTj3zZyFJzgYQdHnqfgI0ddUwS6GqWpsKWhjq9cV0vBAEMN2znq+EBfIWT+pClYw5xsTlJU6GeIBsjGmmANTzJZiIYpgrM0Oa8ZMjd7NP87jxhqGOhJlnQtjuQpB+8aEE00wZFznSJPyHxgH3HkPOsJFvYk8zqCHzTs1BYOa4J3PFU+UVRZxlHDM4YavlNUuMoRveiZA2d7grMNc2g+RbSCEKzmgYsUmWmazFJyoiOZ4KnyhKOGRzWJa0+moyV4TVHDzn51Awtqaphfk/lRQ08FX1iiqxTB/kLwd0VynKfEvI6cd4XMV5bMhZ7gZUWVzYQ6Nm2BYzxJbw3bGthEUUMfgbGeorae6DxHtJoZ6alhZ0+ytiVoK1R4z5PTrOECT/SugseEOlb1MMNR4VRNcJy+V1Hg9ONClSZFZjdHlc6W6FBLdJja2MC5hhpu0DBYEY1TFGwiFAxRRCsYkiM9JRb0JNMVkW6CZYT/2EiTGWmo8k+h4FhDNE7BvppoTSFnmCV5xZKzvcCdDo7VVPnIU+I+Rc68juApC90MwcFCsJ5hDqxgScYKreruyQwTqrzoqDCmhWi4IbhB0Yrt3RGa6GfDv52rKXWhh28dyZaWUvcZeMTBaZoSGyiCtRU5J8iviioHaErs7Jkj61syVzTTgOcUOQ8buFBTYWdL5g3T4qlpe0+wvD63heAXRfCCIed9RbCsp2CiI7raUOYOTU13N8PNHvpaGvayo4a3LLT1lDrVEPT2zLUlheB1R+ZTRfKWJ+dcocLJfi11vyJ51lLqJ0WD7tRwryezjiV5W28uJO9qykzX8JDe2lHl/9oyBwa2UMfOngpXCixvKdXTk3wrsKmiVYdZIqsoWEERjbcUNDuiaQomGoIbFdEHmsyWnuR+IeriKDVLnlawlyNHKwKlSU631PKep8J4Q+ayjkSLKYLhalNHlYvttb6fHm0p6OApsZ4l2VfdqZkjuysy6ysKLlckf1KUutCTs39bmCgEyyoasIWlVaMF7mgmWtBT8Kol5xpH9IGllo8cJdopcvZ2sImlDmMIbtDk3KIpeNiS08lQw11NFPTwVFlPP6pJ2gvRfI7gQUfmNAtf6Gs0wQxDsKGlVBdF8rCa3jzdwMaGHOsItrZk7hAyOzpK9VS06j5F49b0VNGOOfKs3lDToMsMBe9ZWtHFEgxTJLs7qrygKZjUnmCYoeAqeU6jqWuLJup4WghOdvCYJnrSkSzoyRkm5M2StQwVltPkfCAk58tET/CSg+8MUecmotMEnhBKfWBIZsg2ihruMJQaoIm+tkTLKEqspMh00w95gvFCQRtDwTT1gVDDSEVdlwqZfxoQRbK0g+tbiBZxzKlpnpypejdDwTaeOvorMk/IJE10h9CqRe28hhLbe0pMsdSwv4ZbhKivo2BjDWfL8UKJgeavwlwb5KlwhyE4u4XkGE2ytZCznKLCDZZq42VzT8HLCrpruFbIfOIINmh/qCdZ1ZBc65kLHR1Bkyf5zn6pN3SvGKIlFNGplhrO9QSXanLOMQTLCa0YJCRrCZm/CZmrLTm7WzCK4GJDiWUdFeYx1LCFg3NMd0XmCuF3Y5rITLDUsYS9zoHVzwnJoYpSTQoObyEzr4cFBNqYTopoaU/wkyLZ2lPhX/5Y95ulxGTV7KjhWrOZgl8MyUUafjYraNjNU1N3IWcjT5WzWqjwtoarHSUObGYO3GCJZpsBlnJGPd6ZYLyl1GdCA2625IwwJDP8GUKymbzuyPlZlvTUsaUh5zFDhRWFzPKKZLAlWdcQbObgF9tOqOsmB1dqcqYJmWstFbZRRI9poolmqiLnU0POvxScpah2iSL5UJNzgScY5+AuIbpO0YD3NCW+dLMszFSdFCWGqG6eVq2uYVNDdICGD6W7EPRWZEY5gpsE9rUkS3mijzzJnm6UpUFXG1hCUeVoS5WfNcFpblELL2qqrCvMvRfd45oalvKU2tiQ6ePJOVMRXase9iTtLJztPxJKLWpo2CRDcJwn2sWSLKIO1WQWNTCvpVUvOZhgSC40JD0dOctaSqzkCRbXsKlb11Oip6PCJ0IwSJM31j3akRxlP7Rwn6aGaUL0qiLnJkvB3xWZ2+Q1TfCwpQH3G0o92UzmX4o/oJNQMMSQc547wVHhdk+VCw01DFYEnTxzZKAm74QmeNNR1w6WzEhNK15VJzuCdxQ53dRUDws5KvwgBMOEgpcVNe0hZI6RXT1Jd0cyj5nsaEAHgVmGaJIlWdsc5Ui2ElrRR6jrRAttNMEAIWrTDFubkZaok7/AkzfIwfuWVq0jHzuCK4QabtLUMVPB3kJ0oyHTSVFlqMALilJf2Rf8k5aaHtMfayocLBS8L89oKoxpJvnAkDPa0qp5DAUTHKWmCcnthlou8iCKaFFLHWcINd1nyIwXqrSxMNmSs6KmoL2QrKuWtlQ5V0120xQ5vRyZS1rgFkWwhiOwiuQbR0OOVhQM9iS3tiXp4RawRPMp5tDletOOBL95MpM01dZTBM9pkn5qF010rIeHFcFZhmSGpYpTsI6nwhqe5C9ynhlpp5ophuRb6WcJFldkVnVEwwxVfrVkvnWUuNLCg5bgboFHPDlDPDmnK7hUrWiIbjadDclujlZcaokOFup4Ri1kacV6jmrrK1hN9bGwpKEBQ4Q6DvIUXOmo6U5LqQM6EPyiKNjVkPnJkDPNEaxhiFay5ExW1NXVUGqcpYYdPcGiCq7z/TSlbhL4pplWXKd7NZO5QQFrefhRQW/NHOsqcIglc4UhWklR8K0QzbAw08CBDnpbgqXdeD/QUsM4RZXDFBW6WJKe/mFPdH0LtBgiq57wFLzlyQzz82qYx5D5WJP5yVJDW01BfyHnS6HKO/reZqId1WGa4Hkh2kWodJ8i6KoIPlAj2hPt76CzXsVR6koPRzWTfKqIentatYpQw2me4AA3y1Kind3SwoOKZDcFXTwl9tWU6mfgRk9d71sKtlNwrjnYw5tC5n5LdKiGry3JKNlHEd3oaMCFHrazBPMp/uNJ+V7IudcSbeOIdjUEdwl0VHCOZo5t6YluEuaC9mQeMgSfOyKnYGFHcIeQ84yQWbuJYJpZw5CzglDH7gKnWqqM9ZTaXcN0TeYhR84eQtJT76JJ1lREe7WnnvsMmRc9FQ7SBBM9mV3lCUdmHk/S2RAMt0QjFNFqQpWjDPQ01DXWUdDBkXziKPjGEP3VP+zIWU2t7im41FOloyWzn/L6dkUy3VLDaZ6appgDLHPjJEsyvJngWEPUyVBiAaHCTEXwrLvSEbV1e1gKJniicWorC1MUrVjB3uDhJE/wgSOzk1DXpk0k73qCM8xw2UvD5kJmDUfOomqMpWCkJRlvKXGmoeBm18USjVIk04SClxTB6YrgLAPLWYK9HLUt5cmc0vYES8GnTeRc6skZbQkWdxRsIcyBRzx1DbTk9FbU0caTPOgJHhJKnOGIVhQqvKmo0llRw9sabrZkDtdg3PqaKi9oatjY8B+G371paMg6+mZFNNtQ04mWBq3rYLOmtWWQp8KJnpy9DdFensyjdqZ+yY40VJlH8wcdLzC8PZnvHMFUTZUrDTkLyQaGus5X5LzpYAf3i+e/ZlhqGqWhh6Ou6xTR9Z6oi5AZZtp7Mj2EEm8oSpxiYZCHU/1fbGdNNNRRoZMhmilEb2gqHOEJDtXkHK/JnG6IrvbPCwV3NhONVdS1thBMs1T4QOBcTWa2IzhMk2nW5Kyn9tXUtpv9RsG2msxk+ZsQzRQacJncpgke0+T8y5Fzj8BiGo7XlJjaTIlpQs7KFjpqGnKuoyEPeIKnFMkZHvopgh81ySxNFWvJWcKRs70j2FOT012IllEEO1n4pD1513Yg2ssQPOThOkvyrqHUdEXOSEsihmBbTbKX1kLBPWqWkLOqJbjB3GBIZmoa8qWl4CG/iZ7oiA72ZL7TJNeZUY7kFQftDcHHluBzRbCegzMtrRjVQpX2lgoPKKLJAkcbMl01XK2p7yhL8pCBbQ3BN2avJgKvttcrWDK3CiUOVxQ8ZP+pqXKyIxnmBymCg5vJjNfkPK4+c8cIfK8ocVt7kmfd/I5SR1hKvCzUtb+lhgc00ZaO6CyhIQP1Uv4yIZjload72PXX0OIJvnFU+0Zf6MhsJwTfW0r0UwQfW4LNLZl5HK261JCZ4qnBaAreVAS3WrjV0LBnNDUNNDToCEeFfwgcb4gOEqLRhirWkexrCEYKVV711DLYEE1XBEsp5tpTGjorkomKYF9FDXv7fR3BGwbettSxnyL53MBPjsxDZjMh+VUW9NRxq1DhVk+FSxQcaGjV9Pawv6eGByw5qzoy7xk4RsOShqjJwWKe/1pEEfzkobeD/dQJmpqedcyBTy2sr4nGNRH0c0SPWTLrqAc0OQcb/gemKgqucQT7ySWKCn2EUotoCvpZct7RO2sy/QW0IWcXd7pQRQyZVwT2USRO87uhjioTLKV2brpMUcMQRbKH/N2T+UlTpaMls6cmc6CCNy3JdYYSUzzJQ4oSD3oKLncULOiJvjBEC2oqnCJkJluCYy2ZQ5so9YYlZ1VLlQU1mXEW1jZERwj/MUSRc24TdexlqLKfQBtDTScJUV8FszXBEY5ktpD5Ur9hYB4Nb1iikw3JoYpkKX+RodRKFt53MMuRnKSpY31PwYaGaILh3wxJGz9TkTPEETxoCWZrgvOlmyMzxFEwVJE5xZKzvyJ4WxEc16Gd4Xe3Weq4XH2jKRikqOkGQ87hQnC7wBmGYLAnesX3M+S87eFATauuN+Qcrh7xIxXJbUIdMw3JGE3ylCWzrieaqCn4zhGM19TQ3z1oH1AX+pWEqIc7wNGAkULBo/ZxRaV9NNyh4Br3rCHZzbzmSfawBL0dNRwpW1kK9mxPXR9povcdrGSZK9c2k0xwFGzjuniCtRSZCZ6ccZ7gaktmgAOtKbG/JnOkJrjcQTdFMsxRQ2cLY3WTIrlCw1eWKn8R6pvt4GFDso3QoL4a3nLk3G6JrtME3dSenpx7PNFTmga0EaJTLQ061sEeQoWXhSo9LTXsaSjoJQRXeZLtDclbCrYzfzHHeaKjHCVOUkQHO3JeEepr56mhiyaYYKjjNU+Fed1wS5VlhWSqI/hYUdDOkaxiKehoyOnrCV5yBHtbWFqTHCCwtpDcYolesVR5yUzTZBb3RNMd0d6WP+SvhuBmRcGxnuQzT95IC285cr41cLGQ6aJJhmi4TMGempxeimBRQw1tFKV+8jd6KuzoSTqqDxzRtpZkurvKEHxlqXKRIjjfUNNXQsNOsRScoWFLT+YeRZVD3GRN0MdQcKqQjHDMrdGGVu3iYJpQx3WGUvfbmxwFfR20WBq0oYY7LMFhhgYtr8jpaEnaOzjawWWaTP8mMr0t/EPDPoqcnxTBI5o58L7uoWnMrpoqPwgVrlAUWE+V+TQl9rawoyP6QGAlQw2TPRX+YSkxyBC8Z6jhHkXBgQL7WII3DVFnRfCrBfxewv9D6xsyjys4VkhWb9pUU627JllV0YDNHMku/ldNMMXDEo4aFnAkk4U6frNEU4XgZUPmEKHUl44KrzmYamjAbh0JFvGnaTLPu1s9jPCwjFpYiN7z1DTOk/nc07CfDFzmCf7i+bfNHXhDtLeBXzTBT5rkMvWOIxpl4EMh2LGJBu2syDnAEx2naEhHDWMMzPZEhygyS1mS5RTJr5ZkoKbEUoYqr2kqdDUE8ztK7OaIntJkFrIECwv8LJTaVx5XJE86go8dFeZ3FN3rjabCAYpoYEeC9zzJVULBbmZhDyd7ko09ydpNZ3nm2Kee4FPPXHnYEF1nqOFEC08LUVcDvYXkJHW8gTaKCk9YGOeIJhqiE4ToPEepdp7IWFjdwnWaufGMwJJCMtUTTBBK9BGCOy2tGGrJTHIwyEOzp6aPzNMOtlZkDvcEWpP5SVNhfkvDxhmSazTJXYrM9U1E0xwFVwqZQwzJxw6+kGGGUj2FglGGmnb1/G51udRSMNlTw6GGnCcUwVcOpmsqTHa06o72sw1RL02p9z0VbnMLOaIX3QKaYKSCFQzBKEUNHTSc48k53RH9wxGMtpQa5KjjW0W0n6XCCCG4yxNNdhQ4R4l1Ff+2sSd6UFHiIEOyqqFgT01mEUMD+joy75jPhOA+oVVLm309FR4yVOlp4RhLiScNmSmaYF5Pw0STrOIoWMSR2UkRXOMp+M4SHW8o8Zoi6OZgjKOaFar8zZDzkWzvKOjkKBjmCXby8JahhjXULY4KlzgKLvAwxVGhvyd4zxB1d9T0piazmKLCVZY5sKiD0y2ZSYrkUEPUbIk+dlQ4SJHTR50k1DPaUWIdTZW9NJwnJMOECgd7ou/MnppMJ02O1VT4Wsh85MnZzcFTngpXGKo84qmwgKbCL/orR/SzJ2crA+t6Mp94KvxJUeIbT3CQu1uIdlQEOzlKfS3UMcrTiFmOuroocrZrT2AcmamOKg8YomeEKm/rlT2sociMaybaUlFhuqHCM2qIJ+rg4EcDFymiDSxzaHdPcpE62pD5kyM5SBMoA1PaUtfIthS85ig1VPiPPYXgYEMNk4Qq7TXBgo7oT57gPUdwgCHzhIVFPFU6OYJzHAX9m5oNrVjeE61miDrqQ4VSa1oiURTsKHC0IfjNwU2WzK6eqK8jWln4g15TVBnqmDteCJ501PGAocJhhqjZdtBEB6lnhLreFJKxmlKbeGrqLiSThVIbCdGzloasa6lpMQXHCME2boLpJgT7yWaemu6wBONbqGNVRS0PKIL7LckbjmQtR7K8I5qtqel+T/ChJTNIKLjdUMNIRyvOEko9YYl2cwQveBikCNawJKcLBbc7+JM92mysNvd/Fqp8a0k6CNEe7cnZrxlW0wQXaXjaktnRwNOGZKYiONwS7a1JVheq3WgJHlQUGKHKmp4KAxXR/ULURcNgoa4zhKSLpZR3kxRRb0NmD0OFn+UCS7CzI1nbP6+o4x47QZE5xRCt3ZagnYcvmpYQktXdk5YKXTzBC57kKEe0VVuiSYqapssMS3C9p2CKkHOg8B8Pa8p5atrIw3qezIWanMGa5HRDNF6RM9wcacl0N+Q8Z8hsIkSnaIIdHRUOEebAPy1zbCkhM062FCJtif7PU+UtoVXzWKqM1PxXO8cfdruhFQ/a6x3JKYagvVDhQEtNiyiiSQ7OsuRsZUku0CRNDs4Sog6KKjsZgk2bYJqijgsEenoKeniinRXBn/U3lgpPdyDZynQx8IiioMnCep5Ky8mjGs6Wty0l1hUQTcNWswS3WRp2kCNZwJG8omG8JphPUaFbC8lEfabwP7VtM9yoaNCAjpR41VNhrD9LkbN722v0CoZMByFzhaW+MyzRYEWFDQwN2M4/JiT76PuljT3VU/A36eaIThb+R9oZGOAJ9tewkgGvqOMNRWYjT/Cwu99Q8LqDE4TgbLWxJ1jaDDAERsFOFrobgjUsBScaguXU8kKm2RL19tRypSHnHNlHiIZqgufs4opgQdVdwxBNNFBR6kVFqb8ogimOzB6a6HTzrlDHEpYaxjiiA4TMQobkDg2vejjfwJGWmnbVFAw3H3hq2NyQfG7hz4aC+w3BbwbesG0swYayvpAs6++Ri1Vfzx93mFChvyN5xVHTS+0p9aqCAxyZ6ZacZyw5+7uuQkFPR9DDk9NOiE7X1PCYJVjVUqq7JlrHwWALF5nfHNGjApdpqgzx5OwilDhCiDYTgnc9waGW4BdLNNUQvOtpzDOWHDH8D7TR/A/85KljEQu3NREc4Pl/6B1Hhc8Umb5CsKMmGC9EPcxoT2amwHNCmeOEnOPbklnMkbOgIvO5UMOpQrS9UGVdt6iH/fURjhI/WOpaW9OKLYRod6HCUEdOX000wpDZQ6hwg6LgZfOqo1RfT/CrJzjekXOGhpc1VW71ZLbXyyp+93ILbC1kPtIEYx0FIx1VDrLoVzXRKRYWk809yYlC9ImcrinxtabKnzRJk3lAU1OLEN1j2zrYzr2myHRXJFf4h4QKT1qSTzTB5+ZNTzTRkAxX8FcLV2uS8eoQQ2aAkFzvCM72sJIcJET3WPjRk5wi32uSS9rfZajpWEvj9hW42F4o5NytSXYy8IKHay10VYdrcl4SkqscrXpMwyGOgtkajheSxdQqmpxP1L3t4R5PqasFnrQEjytq6qgp9Y09Qx9o4S1FzhUCn1kyHSzBWLemoSGvOqLNhZyBjmCaAUYpMgt4Ck7wBBMMwWKWgjsUwTaGVsxWC1mYoKiyqqeGKYqonSIRQ3KIkHO0pmAxTdBHkbOvfllfr+AA+7gnc50huVKYK393FOyg7rbPO/izI7hE4CnHHHnJ0ogNPRUGeUpsrZZTBJcrovUcJe51BPsr6GkJdhCCsZ6aTtMEb2pqWkqeVtDXE/QVggsU/Nl86d9RMF3DxvZTA58agu810RWawCiSzzXBeU3MMW9oyJUedvNEvQyNu1f10BSMddR1vaLCYpYa/mGocLSiYDcLbQz8aMn5iyF4xBNMs1P0QEOV7o5gaWGuzSeLue4tt3ro7y4Tgm4G/mopdZgl6q0o6KzJWE3mMksNr3r+a6CbT8g5wZNzT9O7fi/zpaOmnz3BRoqos+tv9zMbdpxsqDBOEewtJLt7cg5wtKKbvldpSzRRCD43VFheCI7yZLppggMVBS/KMAdHODJvOwq2NQSbKKKPLdFWQs7Fqo+mpl01JXYRgq8dnGLhTiFzqmWsUMdpllZdbKlyvSdYxhI9YghOtxR8LgSLWHK62mGGVoxzBE8LNWzqH9CUesQzFy5RQzTc56mhi6fgXEWwpKfE5Z7M05ZgZUPmo6auiv8YKzDYwWBLMErIbKHJvOwIrvEdhOBcQ9JdU1NHQ7CXn2XIDFBKU2WAgcX9UAUzDXWd5alwuyJ41Z9rjKLCL4aCp4WarhPm2rH+SaHUYE001JDZ2ZAzXPjdMpZWvC9wmqIB2lLhQ01D5jO06hghWMndbM7yRJMsoCj1vYbnFQVrW9jak3OlEJ3s/96+p33dEPRV5GxiqaGjIthUU6FFEZyqCa5qJrpBdzSw95IUnOPIrCUUjRZQFrbw5PR0R1qiYx3cb6nrWUMrBmmiBQxVHtTew5ICP/ip6g4hed/Akob/32wvBHsIOX83cI8hGeNeNPCIkPmXe8fPKx84OMSRM1MTdXSwjCZ4S30jVGhvqTRak/OVhgGazHuOCud5onEO1lJr6ecVyaOK6H7zqlBlIaHE0oroCgfvGJIdPcmfLNGLjpz7hZwZQpUbFME0A1cIJa7VNORkgfsMBatbKgwwJM9bSvQXeNOvbIjelg6WWvo5kvbKaJJNHexkKNHL9xRyFlH8Ti2riB5wVPhUk7nGkJnoCe428LR/wRGdYIlmWebCyxou1rCk4g/ShugBDX0V0ZQWkh0dOVsagkM0yV6OoLd5ye+pRlsCr0n+KiQrGuq5yJDzrTAXHtLUMduTDBVKrSm3eHL+6ijxhFDX9Z5gVU/wliHYTMiMFpKLNMEywu80wd3meoFmt6VbRMPenhrOc6DVe4pgXU8DnnHakLOIIrlF4FZPIw6R+zxBP0dyq6OOZ4Q5sLKCcz084ok+VsMMyQhNZmmBgX5xIXOEJTmi7VsGTvMTNdHHhpzdbE8Du2oKxgvBqQKdDDnTFOylCFaxR1syz2iqrOI/FEpNc3C6f11/7+ASS6l2inq2ciTrCCzgyemrCL5SVPjQkdPZUmGy2c9Sw9FtR1sS30RmsKPCS4rkIC/2U0MduwucYolGaPjKEyhzmiPYXagyWbYz8LWBDdzRimAXzxx4z8K9hpzlhLq+NiQ97HuKorMUfK/OVvC2JfiHUPCQI/q7J2gjK+tTDNxkCc4TMssqCs4TGtLVwQihyoAWgj9bosU80XGW6Ac9TJGziaUh5+hnFcHOnlaM1iRn29NaqGENTTTSUHCH2tWTeV0osUhH6psuVLjRUmGWhm6OZEshGeNowABHcJ2Bpy2ZszRcKkRXd2QuKVEeXnbfaEq825FguqfgfE2whlChSRMdron+LATTPQ2Z369t4B9C5gs/ylzv+CMmepIDPclFQl13W0rspPd1JOcbghGOEutqCv5qacURQl3dDKyvyJlqKXGPgcM9FfawJAMVmdcspcYKOZc4GjDYkFlK05olNMHyHn4zFNykyOxt99RkHlfwmiHo60l2EKI+mhreEKp080Tbug08BVPcgoqC5zWt+NLDTZ7oNSF51N1qie7Va3uCCwyZbkINf/NED6jzOsBdZjFN8oqG3wxVunqCSYYKf3EdhJyf9YWGf7tRU2oH3VHgPr1fe5J9hOgHd7xQ0y7qBwXr23aGErP0cm64JVjZwsOGqL+mhNgZmhJLW2oY4UhedsyBgzrCKrq7BmcpNVhR6jBPq64Vgi+kn6XE68pp8J5/+0wRHGOpsKenQn9DZntPzjRLZpDAdD2fnSgkG9tmIXnUwQ6WVighs7Yi2MxQ0N3CqYaCXkJ0oyOztMDJjmSSpcpvlrk0RMMOjmArQ04PRV1DO1FwhCVaUVPpKUM03JK5SxPsIWRu8/CGHi8UHChiqGFDTbSRJWeYUDDcH6vJWUxR4k1FXbMUwV6e4AJFXS8oMqsZKqzvYQ9DDQdZckY4aGsIhtlubbd2r3j4QBMoTamdPZk7O/Bf62lacZwneNjQoGcdVU7zJOd7ghsUHOkosagic6cnWc8+4gg285R6zZP5s1/LUbCKIznTwK36PkdwlOrl4U1LwfdCCa+IrvFkmgw1PCAUXKWo0sURXWcI2muKJlgyFzhynCY4RBOsqCjoI1R5zREco0n2Vt09BQtYSizgKNHfUmUrQ5UOCh51BFcLmY7umhYqXKQomOop8bUnWNNQcIiBcYaC6xzMNOS8JQQfeqKBmmglB+97ok/lfk3ygaHSyZaCRTzRxQo6GzLfa2jWBPepw+UmT7SQEJyiyRkhBLMVOfcoMjcK0eZChfUNzFAUzCsEN5vP/X1uP/n/aoMX+K+nw/Hjr/9xOo7j7Pju61tLcgvJpTWXNbfN5jLpi6VfCOviTktKlFusQixdEKWmEBUKNaIpjZRSSOXSgzaaKLdabrm1/9nZ+/f+vd/vz/v9+Xy+zZ7PRorYoZqyLrCwQdEAixxVOEXNNnjX2nUSRlkqGmWowk8lxR50JPy9Bo6qJXaXwNvREBvnThPEPrewryLhcAnj5WE15Fqi8W7R1sAuEu86S4ENikItFN4xkv9Af4nXSnUVcLiA9xzesFpivRRVeFKtsMRaKBhuSbjOELnAUtlSQUpXgdfB4Z1oSbnFEetbQ0IrAe+Y+pqnDcEJFj6S8LDZzZHwY4e3XONNlARraomNEt2bkvGsosA3ioyHm+6jCMbI59wqt4eeara28IzEmyPgoRaUOEDhTVdEJhmCoTWfC0p8aNkCp0oYqih2iqGi4yXeMkOsn4LdLLnmKfh/YogjNsPebeFGR4m9BJHLzB61XQ3BtpISfS2FugsK9FAtLWX1dCRcrCnUp44CNzuCowUZmxSRgYaE6Za0W2u/E7CVXCiI/UOR8aAm1+OSyE3mOUcwyc1zBBeoX1kiKy0Zfxck1Gsyulti11i83QTBF5Kg3pDQThFMVHiPSlK+0cSedng/VaS8bOZbtsBcTcZAR8JP5KeqQ1OYKAi20njdNNRpgnsU//K+JnaXJaGTomr7aYIphoRn9aeShJWKEq9LcozSF7QleEfDI5LYm5bgVkFkRwVDBCVu0DDIkGupo8TZBq+/pMQURYErJQmPKGKjNDkWOLx7Jd5QizdUweIaKrlP7SwJDhZvONjLkOsBBX9UpGxnydhXkfBLQ8IxgojQbLFnJf81JytSljclYYyEFyx0kVBvKWOFJmONpshGAcsduQY5giVNCV51eOdJYo/pLhbvM0uDHSevNKRcrKZIqnCtJeEsO95RoqcgGK4ocZcho1tTYtcZvH41pNQ7vA0WrhIfOSraIIntIAi+NXWCErdbkvrWwjRLrt0NKUdL6KSOscTOdMSOUtBHwL6OLA0vNSdynaWQEnCpIvKaIrJJEbvHkmuNhn6OjM8VkSGSqn1uYJCGHnq9I3aLhNME3t6GjIkO7xrNFumpyTNX/NrwX7CrIRiqqWijI9JO4d1iieykyfiposQIQ8YjjsjlBh6oHWbwRjgYJQn2NgSnNycmJAk3NiXhx44Sxykihxm8ybUwT1OVKySc7vi3OXVkdBJ4AyXBeksDXG0IhgtYY0lY5ahCD0ehborIk5aUWRJviMA7Xt5kyRjonrXENkm8yYqgs8VzgrJmClK20uMM3jRJ0FiQICQF9hdETlLQWRIb5ki6WDfWRPobvO6a4GP5mcOrNzDFELtTkONLh9dXE8xypEg7z8A9jkhrQ6Fhjlg/QVktJXxt4WXzT/03Q8IaQWSqIuEvloQ2mqC9Jfi7wRul4RX3pSPlzpoVlmCtI2jvKHCFhjcM3sN6lqF6HxnKelLjXWbwrpR4xzuCrTUZx2qq9oAh8p6ixCUGr78g8oyjRAtB5CZFwi80VerVpI0h+IeBxa6Zg6kWvpDHaioYYuEsRbDC3eOmC2JvGYLeioxGknL2UATNJN6hmtj1DlpLvDVmocYbrGCVJKOrg4X6DgddLA203BKMFngdJJFtFd7vJLm6KEpc5yjQrkk7M80SGe34X24nSex1Ra5Omgb71JKyg8SrU3i/kARKwWpH0kOGhKkObyfd0ZGjvyXlAkVZ4xRbYJ2irFMkFY1SwyWxr2oo4zlNiV+7zmaweFpT4kR3kaDAFW6xpSqzJay05FtYR4HmZhc9UxKbbfF2V8RG1MBmSaE+kmC6JnaRXK9gsiXhJHl/U0qM0WTcbyhwkYIvFGwjSbjfwhiJt8ZSQU+Bd5+marPMOkVkD0muxYLIfEuhh60x/J92itguihJSEMySVPQnTewnEm+620rTQEMsOfo4/kP/0ARvWjitlpSX7GxBgcMEsd3EEeYWvdytd+Saawi6aCIj1CkGb6Aj9rwhx16Cf3vAwFy5pyLhVonXzy51FDpdEblbkdJbUcEPDEFzQ8qNmhzzLTmmKWKbFCXeEuRabp6rxbvAtLF442QjQ+wEA9eL1xSR7Q0JXzlSHjJ4exq89yR0laScJ/FW6z4a73pFMEfDiRZvuvijIt86RaSFOl01riV2mD1UEvxGk/Geg5aWwGki1zgKPG9J2U8PEg8qYvMsZeytiTRXBMslCU8JSlxi8EabjwUldlDNLfzTUmCgxWsjqWCOHavYAqsknKFIO0yQ61VL5AVFxk6WhEaCAkdJgt9aSkzXlKNX2jEa79waYuc7gq0N3GDJGCBhoiTXUEPsdknCUE1CK0fwsiaylSF2uiDyO4XX3pFhNd7R4itFGc0k/ElBZwWvq+GC6szVeEoS/MZ+qylwpKNKv9Z469UOjqCjwlusicyTxG6VpNxcQ8IncoR4RhLbR+NdpGGmJWOcIzJGUuKPGpQg8rrG21dOMqQssJQ4RxH5jaUqnZuQ0F4Q+cjxLwPtpZbIAk3QTJHQWBE5S1BokoVtDd6lhqr9UpHSUxMcIYl9pojsb8h4SBOsMQcqvOWC2E8EVehqiJ1hrrAEbQxeK0NGZ0Gkq+guSRgniM23bIHVkqwx4hiHd7smaOyglyIyQuM978j4VS08J/A2G1KeMBRo4fBaSNhKUEZfQewVQ/C1I+MgfbEleEzCUw7mKXI0M3hd1EESVji8x5uQ41nxs1q4RMJCCXs7Iq9acpxn22oSDnQ/sJTxsCbHIYZiLyhY05TY0ZLIOQrGaSJDDN4t8pVaIrsqqFdEegtizc1iTew5Q4ayBDMUsQMkXocaYkc0hZua412siZ1rSXlR460zRJ5SlHGe5j801RLMlJTxtaOM3Q1pvxJ45zUlWFD7rsAbpfEm1JHxG0eh8w2R7QQVzBUw28FhFp5QZzq8t2rx2joqulYTWSuJdTYfWwqMFMcovFmSyJPNyLhE4E10pHzYjOC3huArRa571ZsGajQpQx38SBP5pyZB6lMU3khDnp0MBV51BE9o2E+TY5Ml2E8S7C0o6w1xvCZjf0HkVEHCzFoyNmqC+9wdcqN+Tp7jSDheE9ws8Y5V0NJCn2bk2tqSY4okdrEhx1iDN8cSudwepWmAGXKcJXK65H9to8jYQRH7SBF01ESUJdd0TayVInaWhLkOjlXE5irKGOnI6GSWGCJa482zBI9rCr0jyTVcEuzriC1vcr6mwFGSiqy5zMwxBH/TJHwjSPhL8+01kaaSUuMFKTcLEvaUePcrSmwn8DZrgikWb7CGPxkSjhQwrRk57tctmxLsb9sZvL9LSlyuSLlWkqOjwduo8b6Uv1DkmudIeFF2dHCgxVtk8dpIvHpBxhEOdhKk7OLIUSdJ+cSRY57B+0DgGUUlNfpthTfGkauzxrvTsUUaCVhlKeteTXCoJDCa2NOKhOmC4G1H8JBd4OBZReSRGkqcb/CO1PyLJTLB4j1q8JYaIutEjSLX8YKM+a6phdMsdLFUoV5RTm9JSkuDN8WcIon0NZMNZWh1q8C7SJEwV5HxrmnnTrf3KoJBlmCYI2ilSLlfEvlE4011NNgjgthzEua0oKK7JLE7HZHlEl60BLMVFewg4EWNt0ThrVNEVkkiTwpKXSWJzdRENgvKGq4IhjsiezgSFtsfCUq8qki5S1LRQeYQQ4nemmCkImWMw3tFUoUBZk4NOeZYEp4XRKTGa6wJjrWNHBVJR4m3FCnbuD6aak2WsMTh3SZImGCIPKNgsDpVwnsa70K31lCFJZYcwwSMFcQulGTsZuEaSdBXkPGZhu0FsdUO73RHjq8MPGGIfaGIbVTk6iuI3GFgucHrIQkmWSJdBd7BBu+uOryWAhY7+Lki9rK5wtEQzWwvtbqGhIMFwWRJsElsY4m9IIg9L6lCX0VklaPAYkfkZEGDnOWowlBJjtMUkcGK4Lg6EtoZInMUBVYLgn0UsdmCyCz7gIGHFfk+k1QwTh5We7A9x+IdJ6CvIkEagms0hR50eH9UnTQJ+2oiKyVlLFUE+8gBGu8MQ3CppUHesnjTHN4QB/UGPhCTHLFPHMFrCqa73gqObUJGa03wgbhHkrCfpEpzNLE7JDS25FMKhlhKKWKfCgqstLCPu1zBXy0J2ztwjtixBu8UTRn9LVtkmCN2iyFhtME70JHRQ1KVZXqKI/KNIKYMCYs1GUMEKbM1bKOI9LDXC7zbHS+bt+1MTWS9odA9DtrYtpbImQJ2VHh/lisEwaHqUk1kjKTAKknkBEXkbkdMGwq0dnhzLJF3NJH3JVwrqOB4Sca2hti75nmJN0WzxS6UxDYoEpxpa4htVlRjkYE7DZGzJVU72uC9IyhQL4i8YfGWSYLLNcHXloyz7QhNifmKSE9JgfGmuyLhc403Xm9vqcp6gXe3xuuv8F6VJNxkyTHEkHG2g0aKXL0MsXc1bGfgas2//dCONXiNLCX+5mB7eZIl1kHh7ajwpikyzlUUWOVOsjSQlsS+M0R+pPje/dzBXRZGO0rMtgQrLLG9VSu9n6CMXS3BhwYmSoIBhsjNBmZbgusE9BCPCP5triU4VhNbJfE+swSP27aayE8tuTpYYjtrYjMVGZdp2NpS1s6aBnKSHDsbKuplKbHM4a0wMFd/5/DmGyKrJSUaW4IBrqUhx0vyfzTBBLPIUcnZdrAkNsKR0sWRspumSns6Ch0v/qqIbBYUWKvPU/CFoyrDJGwSNFhbA/MlzKqjrO80hRbpKx0Jewsi/STftwGSlKc1JZyAzx05dhLEdnfQvhZOqiHWWEAHC7+30FuRcZUgaO5gpaIK+xsiHRUsqaPElTV40xQZQ107Q9BZE1nryDVGU9ZSQ47bmhBpLcYpUt7S+xuK/FiT8qKjwXYw5ypS2iuCv7q1gtgjhuBuB8LCFY5cUuCNtsQOFcT+4Ih9JX+k8Ea6v0iCIRZOtCT0Et00JW5UeC85Cg0ScK0k411HcG1zKtre3SeITBRk7WfwDhEvaYLTHP9le0m8By0JDwn4TlLW/aJOvGHxdjYUes+ScZigCkYQdNdEOhkiezgShqkx8ueKjI8lDfK2oNiOFvrZH1hS+tk7NV7nOmLHicGWEgubkXKdwdtZknCLJXaCpkrjZBtLZFsDP9CdxWsSr05Sxl6CMmoFbCOgryX40uDtamB7SVmXW4Ihlgpmq+00tBKUUa83WbjLUNkzDmY7cow1JDygyPGlhgGKYKz4vcV7QBNbJIgM11TUqZaMdwTeSguH6rOaw1JRKzaaGyxVm2EJ/uCIrVWUcZUkcp2grMsEjK+DMwS59jQk3Kd6SEq1d0S6uVmO4Bc1lDXTUcHjluCXEq+1OlBDj1pi9zgiXxnKuE0SqTXwhqbETW6RggMEnGl/q49UT2iCzgJvRwVXS2K/d6+ZkyUl7jawSVLit46EwxVljDZwoSQ20sDBihztHfk2yA8NVZghiXwrYHQdfKAOtzsayjhY9bY0yE2CWEeJ9xfzO423xhL5syS2TFJofO2pboHob0nY4GiAgRrvGQEDa/FWSsoaaYl0syRsEt3kWoH3B01shCXhTUWe9w3Bt44SC9QCh3eShQctwbaK2ApLroGCMlZrYqvlY3qYhM0aXpFkPOuoqJ3Dm6fxXrGwVF9gCWZagjPqznfkuMKQ8DPTQRO8ZqG1hPGKEm9IgpGW4DZDgTNriTxvFiq+Lz+0cKfp4wj6OCK9JSnzNSn9LFU7UhKZZMnYwcJ8s8yRsECScK4j5UOB95HFO0CzhY4xJxuCix0lDlEUeMdS6EZBkTsUkZ4K74dugyTXS7aNgL8aqjDfkCE0ZbwkCXpaWCKhl8P7VD5jxykivSyxyZrYERbe168LYu9ZYh86IkscgVLE7tWPKmJv11CgoyJltMEbrohtVAQfO4ImltiHEroYEs7RxAarVpY8AwXMcMReFOTYWe5iiLRQxJ5Q8DtJ8LQhWOhIeFESPGsILhbNDRljNbHzNRlTFbk2S3L0NOS6V1KFJYKUbSTcIIhM0wQ/s2TM0SRMNcQmSap3jCH4yhJZKSkwyRHpYYgsFeQ4U7xoCB7VVOExhXepo9ABBsYbvGWKXPME3lyH95YioZ0gssQRWWbI+FaSMkXijZXwgiTlYdPdkNLaETxlyDVIwqeaEus0aTcYcg0RVOkpR3CSJqIddK+90JCxzsDVloyrFd5ZAr4TBKfaWa6boEA7C7s6EpYaeFPjveooY72mjIccLHJ9HUwVlDhKkmutJDJBwnp1rvulJZggKDRfbXAkvC/4l3ozQOG9a8lxjx0i7nV4jSXc7vhe3OwIxjgSHjdEhhsif9YkPGlus3iLFDnWOFhtCZbJg0UbQcIaR67JjthoCyMEZRwhiXWyxO5QxI6w5NhT4U1WsJvDO60J34fW9hwzwlKij6ZAW9ne4L0s8C6XeBMEkd/LQy1VucBRot6QMlbivaBhoBgjqGiCJNhsqVp/S2SsG6DIONCR0dXhvWbJ+MRRZJkkuEjgDXJjFQW6SSL7GXK8Z2CZg7cVsbWGoKmEpzQ5elpiy8Ryg7dMkLLUEauzeO86CuwlSOlgYLojZWeJ9xM3S1PWfEfKl5ISLQ0MEKR8YOB2QfCxJBjrKPCN4f9MkaSsqoVXJBmP7EpFZ9UQfOoOFwSzBN4MQ8LsGrymlipcJQhmy0GaQjPqCHaXRwuCZwRbqK2Fg9wlClZqYicrIgMdZfxTQ0c7TBIbrChxmuzoKG8XRaSrIhhiyNFJkrC7oIAWMEOQa5aBekPCRknCo4IKPrYkvCDI8aYmY7WFtprgekcJZ3oLIqssCSMtFbQTJKwXYy3BY5oCh2iKPCpJOE+zRdpYgi6O2KmOAgvVCYaU4ySRek1sgyFhJ403QFHiVEmJHwtybO1gs8Hr5+BETQX3War0qZngYGgtVZtoqd6vFSk/UwdZElYqyjrF4HXUeFspIi9IGKf4j92pKGAdCYMVsbcV3kRF0N+R8LUd5PCsIGWoxDtBkCI0nKofdJQxT+LtZflvuc8Q3CjwWkq8KwUpHzkK/NmSsclCL0nseQdj5FRH5CNHSgtLiW80Of5HU9Hhlsga9bnBq3fEVltKfO5IaSTmGjjc4J0otcP7QsJUSQM8pEj5/wCuUuC2DWz8AAAAAElFTkSuQmCC\");\
+}\
+.ace-ambiance .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQUFD4z6Crq/sfAAuYAuYl+7lfAAAAAElFTkSuQmCC\") right repeat-y;\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-chaos.js b/services/web/public/js/ace-1.2.5/theme-chaos.js
new file mode 100644
index 0000000000..97ec7fbdc7
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-chaos.js
@@ -0,0 +1,156 @@
+ace.define("ace/theme/chaos",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-chaos";
+exports.cssText = ".ace-chaos .ace_gutter {\
+background: #141414;\
+color: #595959;\
+border-right: 1px solid #282828;\
+}\
+.ace-chaos .ace_gutter-cell.ace_warning {\
+background-image: none;\
+background: #FC0;\
+border-left: none;\
+padding-left: 0;\
+color: #000;\
+}\
+.ace-chaos .ace_gutter-cell.ace_error {\
+background-position: -6px center;\
+background-image: none;\
+background: #F10;\
+border-left: none;\
+padding-left: 0;\
+color: #000;\
+}\
+.ace-chaos .ace_print-margin {\
+border-left: 1px solid #555;\
+right: 0;\
+background: #1D1D1D;\
+}\
+.ace-chaos {\
+background-color: #161616;\
+color: #E6E1DC;\
+}\
+.ace-chaos .ace_cursor {\
+border-left: 2px solid #FFFFFF;\
+}\
+.ace-chaos .ace_cursor.ace_overwrite {\
+border-left: 0px;\
+border-bottom: 1px solid #FFFFFF;\
+}\
+.ace-chaos .ace_marker-layer .ace_selection {\
+background: #494836;\
+}\
+.ace-chaos .ace_marker-layer .ace_step {\
+background: rgb(198, 219, 174);\
+}\
+.ace-chaos .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #FCE94F;\
+}\
+.ace-chaos .ace_marker-layer .ace_active-line {\
+background: #333;\
+}\
+.ace-chaos .ace_gutter-active-line {\
+background-color: #222;\
+}\
+.ace-chaos .ace_invisible {\
+color: #404040;\
+}\
+.ace-chaos .ace_keyword {\
+color:#00698F;\
+}\
+.ace-chaos .ace_keyword.ace_operator {\
+color:#FF308F;\
+}\
+.ace-chaos .ace_constant {\
+color:#1EDAFB;\
+}\
+.ace-chaos .ace_constant.ace_language {\
+color:#FDC251;\
+}\
+.ace-chaos .ace_constant.ace_library {\
+color:#8DFF0A;\
+}\
+.ace-chaos .ace_constant.ace_numeric {\
+color:#58C554;\
+}\
+.ace-chaos .ace_invalid {\
+color:#FFFFFF;\
+background-color:#990000;\
+}\
+.ace-chaos .ace_invalid.ace_deprecated {\
+color:#FFFFFF;\
+background-color:#990000;\
+}\
+.ace-chaos .ace_support {\
+color: #999;\
+}\
+.ace-chaos .ace_support.ace_function {\
+color:#00AEEF;\
+}\
+.ace-chaos .ace_function {\
+color:#00AEEF;\
+}\
+.ace-chaos .ace_string {\
+color:#58C554;\
+}\
+.ace-chaos .ace_comment {\
+color:#555;\
+font-style:italic;\
+padding-bottom: 0px;\
+}\
+.ace-chaos .ace_variable {\
+color:#997744;\
+}\
+.ace-chaos .ace_meta.ace_tag {\
+color:#BE53E6;\
+}\
+.ace-chaos .ace_entity.ace_other.ace_attribute-name {\
+color:#FFFF89;\
+}\
+.ace-chaos .ace_markup.ace_underline {\
+text-decoration: underline;\
+}\
+.ace-chaos .ace_fold-widget {\
+text-align: center;\
+}\
+.ace-chaos .ace_fold-widget:hover {\
+color: #777;\
+}\
+.ace-chaos .ace_fold-widget.ace_start,\
+.ace-chaos .ace_fold-widget.ace_end,\
+.ace-chaos .ace_fold-widget.ace_closed{\
+background: none;\
+border: none;\
+box-shadow: none;\
+}\
+.ace-chaos .ace_fold-widget.ace_start:after {\
+content: '▾'\
+}\
+.ace-chaos .ace_fold-widget.ace_end:after {\
+content: '▴'\
+}\
+.ace-chaos .ace_fold-widget.ace_closed:after {\
+content: '‣'\
+}\
+.ace-chaos .ace_indent-guide {\
+border-right:1px dotted #333;\
+margin-right:-1px;\
+}\
+.ace-chaos .ace_fold { \
+background: #222; \
+border-radius: 3px; \
+color: #7AF; \
+border: none; \
+}\
+.ace-chaos .ace_fold:hover {\
+background: #CCC; \
+color: #000;\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-chrome.js b/services/web/public/js/ace-1.2.5/theme-chrome.js
new file mode 100644
index 0000000000..83742aa464
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-chrome.js
@@ -0,0 +1,128 @@
+ace.define("ace/theme/chrome",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-chrome";
+exports.cssText = ".ace-chrome .ace_gutter {\
+background: #ebebeb;\
+color: #333;\
+overflow : hidden;\
+}\
+.ace-chrome .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-chrome {\
+background-color: #FFFFFF;\
+color: black;\
+}\
+.ace-chrome .ace_cursor {\
+color: black;\
+}\
+.ace-chrome .ace_invisible {\
+color: rgb(191, 191, 191);\
+}\
+.ace-chrome .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-chrome .ace_constant.ace_language {\
+color: rgb(88, 92, 246);\
+}\
+.ace-chrome .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-chrome .ace_invalid {\
+background-color: rgb(153, 0, 0);\
+color: white;\
+}\
+.ace-chrome .ace_fold {\
+}\
+.ace-chrome .ace_support.ace_function {\
+color: rgb(60, 76, 114);\
+}\
+.ace-chrome .ace_support.ace_constant {\
+color: rgb(6, 150, 14);\
+}\
+.ace-chrome .ace_support.ace_type,\
+.ace-chrome .ace_support.ace_class\
+.ace-chrome .ace_support.ace_other {\
+color: rgb(109, 121, 222);\
+}\
+.ace-chrome .ace_variable.ace_parameter {\
+font-style:italic;\
+color:#FD971F;\
+}\
+.ace-chrome .ace_keyword.ace_operator {\
+color: rgb(104, 118, 135);\
+}\
+.ace-chrome .ace_comment {\
+color: #236e24;\
+}\
+.ace-chrome .ace_comment.ace_doc {\
+color: #236e24;\
+}\
+.ace-chrome .ace_comment.ace_doc.ace_tag {\
+color: #236e24;\
+}\
+.ace-chrome .ace_constant.ace_numeric {\
+color: rgb(0, 0, 205);\
+}\
+.ace-chrome .ace_variable {\
+color: rgb(49, 132, 149);\
+}\
+.ace-chrome .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-chrome .ace_entity.ace_name.ace_function {\
+color: #0000A2;\
+}\
+.ace-chrome .ace_heading {\
+color: rgb(12, 7, 255);\
+}\
+.ace-chrome .ace_list {\
+color:rgb(185, 6, 144);\
+}\
+.ace-chrome .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-chrome .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-chrome .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-chrome .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-chrome .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.07);\
+}\
+.ace-chrome .ace_gutter-active-line {\
+background-color : #dcdcdc;\
+}\
+.ace-chrome .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-chrome .ace_storage,\
+.ace-chrome .ace_keyword,\
+.ace-chrome .ace_meta.ace_tag {\
+color: rgb(147, 15, 128);\
+}\
+.ace-chrome .ace_string.ace_regex {\
+color: rgb(255, 0, 0)\
+}\
+.ace-chrome .ace_string {\
+color: #1A1AA6;\
+}\
+.ace-chrome .ace_entity.ace_other.ace_attribute-name {\
+color: #994409;\
+}\
+.ace-chrome .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-clouds.js b/services/web/public/js/ace-1.2.5/theme-clouds.js
new file mode 100644
index 0000000000..83d0d14d59
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-clouds.js
@@ -0,0 +1,95 @@
+ace.define("ace/theme/clouds",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-clouds";
+exports.cssText = ".ace-clouds .ace_gutter {\
+background: #ebebeb;\
+color: #333\
+}\
+.ace-clouds .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8\
+}\
+.ace-clouds {\
+background-color: #FFFFFF;\
+color: #000000\
+}\
+.ace-clouds .ace_cursor {\
+color: #000000\
+}\
+.ace-clouds .ace_marker-layer .ace_selection {\
+background: #BDD5FC\
+}\
+.ace-clouds.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #FFFFFF;\
+}\
+.ace-clouds .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0)\
+}\
+.ace-clouds .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #BFBFBF\
+}\
+.ace-clouds .ace_marker-layer .ace_active-line {\
+background: #FFFBD1\
+}\
+.ace-clouds .ace_gutter-active-line {\
+background-color : #dcdcdc\
+}\
+.ace-clouds .ace_marker-layer .ace_selected-word {\
+border: 1px solid #BDD5FC\
+}\
+.ace-clouds .ace_invisible {\
+color: #BFBFBF\
+}\
+.ace-clouds .ace_keyword,\
+.ace-clouds .ace_meta,\
+.ace-clouds .ace_support.ace_constant.ace_property-value {\
+color: #AF956F\
+}\
+.ace-clouds .ace_keyword.ace_operator {\
+color: #484848\
+}\
+.ace-clouds .ace_keyword.ace_other.ace_unit {\
+color: #96DC5F\
+}\
+.ace-clouds .ace_constant.ace_language {\
+color: #39946A\
+}\
+.ace-clouds .ace_constant.ace_numeric {\
+color: #46A609\
+}\
+.ace-clouds .ace_constant.ace_character.ace_entity {\
+color: #BF78CC\
+}\
+.ace-clouds .ace_invalid {\
+background-color: #FF002A\
+}\
+.ace-clouds .ace_fold {\
+background-color: #AF956F;\
+border-color: #000000\
+}\
+.ace-clouds .ace_storage,\
+.ace-clouds .ace_support.ace_class,\
+.ace-clouds .ace_support.ace_function,\
+.ace-clouds .ace_support.ace_other,\
+.ace-clouds .ace_support.ace_type {\
+color: #C52727\
+}\
+.ace-clouds .ace_string {\
+color: #5D90CD\
+}\
+.ace-clouds .ace_comment {\
+color: #BCC8BA\
+}\
+.ace-clouds .ace_entity.ace_name.ace_tag,\
+.ace-clouds .ace_entity.ace_other.ace_attribute-name {\
+color: #606060\
+}\
+.ace-clouds .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-clouds_midnight.js b/services/web/public/js/ace-1.2.5/theme-clouds_midnight.js
new file mode 100644
index 0000000000..275e9f296a
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-clouds_midnight.js
@@ -0,0 +1,96 @@
+ace.define("ace/theme/clouds_midnight",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-clouds-midnight";
+exports.cssText = ".ace-clouds-midnight .ace_gutter {\
+background: #232323;\
+color: #929292\
+}\
+.ace-clouds-midnight .ace_print-margin {\
+width: 1px;\
+background: #232323\
+}\
+.ace-clouds-midnight {\
+background-color: #191919;\
+color: #929292\
+}\
+.ace-clouds-midnight .ace_cursor {\
+color: #7DA5DC\
+}\
+.ace-clouds-midnight .ace_marker-layer .ace_selection {\
+background: #000000\
+}\
+.ace-clouds-midnight.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #191919;\
+}\
+.ace-clouds-midnight .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-clouds-midnight .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #BFBFBF\
+}\
+.ace-clouds-midnight .ace_marker-layer .ace_active-line {\
+background: rgba(215, 215, 215, 0.031)\
+}\
+.ace-clouds-midnight .ace_gutter-active-line {\
+background-color: rgba(215, 215, 215, 0.031)\
+}\
+.ace-clouds-midnight .ace_marker-layer .ace_selected-word {\
+border: 1px solid #000000\
+}\
+.ace-clouds-midnight .ace_invisible {\
+color: #666\
+}\
+.ace-clouds-midnight .ace_keyword,\
+.ace-clouds-midnight .ace_meta,\
+.ace-clouds-midnight .ace_support.ace_constant.ace_property-value {\
+color: #927C5D\
+}\
+.ace-clouds-midnight .ace_keyword.ace_operator {\
+color: #4B4B4B\
+}\
+.ace-clouds-midnight .ace_keyword.ace_other.ace_unit {\
+color: #366F1A\
+}\
+.ace-clouds-midnight .ace_constant.ace_language {\
+color: #39946A\
+}\
+.ace-clouds-midnight .ace_constant.ace_numeric {\
+color: #46A609\
+}\
+.ace-clouds-midnight .ace_constant.ace_character.ace_entity {\
+color: #A165AC\
+}\
+.ace-clouds-midnight .ace_invalid {\
+color: #FFFFFF;\
+background-color: #E92E2E\
+}\
+.ace-clouds-midnight .ace_fold {\
+background-color: #927C5D;\
+border-color: #929292\
+}\
+.ace-clouds-midnight .ace_storage,\
+.ace-clouds-midnight .ace_support.ace_class,\
+.ace-clouds-midnight .ace_support.ace_function,\
+.ace-clouds-midnight .ace_support.ace_other,\
+.ace-clouds-midnight .ace_support.ace_type {\
+color: #E92E2E\
+}\
+.ace-clouds-midnight .ace_string {\
+color: #5D90CD\
+}\
+.ace-clouds-midnight .ace_comment {\
+color: #3C403B\
+}\
+.ace-clouds-midnight .ace_entity.ace_name.ace_tag,\
+.ace-clouds-midnight .ace_entity.ace_other.ace_attribute-name {\
+color: #606060\
+}\
+.ace-clouds-midnight .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-cobalt.js b/services/web/public/js/ace-1.2.5/theme-cobalt.js
new file mode 100644
index 0000000000..c5b6f267c2
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-cobalt.js
@@ -0,0 +1,113 @@
+ace.define("ace/theme/cobalt",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-cobalt";
+exports.cssText = ".ace-cobalt .ace_gutter {\
+background: #011e3a;\
+color: rgb(128,145,160)\
+}\
+.ace-cobalt .ace_print-margin {\
+width: 1px;\
+background: #555555\
+}\
+.ace-cobalt {\
+background-color: #002240;\
+color: #FFFFFF\
+}\
+.ace-cobalt .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-cobalt .ace_marker-layer .ace_selection {\
+background: rgba(179, 101, 57, 0.75)\
+}\
+.ace-cobalt.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #002240;\
+}\
+.ace-cobalt .ace_marker-layer .ace_step {\
+background: rgb(127, 111, 19)\
+}\
+.ace-cobalt .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(255, 255, 255, 0.15)\
+}\
+.ace-cobalt .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.35)\
+}\
+.ace-cobalt .ace_gutter-active-line {\
+background-color: rgba(0, 0, 0, 0.35)\
+}\
+.ace-cobalt .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(179, 101, 57, 0.75)\
+}\
+.ace-cobalt .ace_invisible {\
+color: rgba(255, 255, 255, 0.15)\
+}\
+.ace-cobalt .ace_keyword,\
+.ace-cobalt .ace_meta {\
+color: #FF9D00\
+}\
+.ace-cobalt .ace_constant,\
+.ace-cobalt .ace_constant.ace_character,\
+.ace-cobalt .ace_constant.ace_character.ace_escape,\
+.ace-cobalt .ace_constant.ace_other {\
+color: #FF628C\
+}\
+.ace-cobalt .ace_invalid {\
+color: #F8F8F8;\
+background-color: #800F00\
+}\
+.ace-cobalt .ace_support {\
+color: #80FFBB\
+}\
+.ace-cobalt .ace_support.ace_constant {\
+color: #EB939A\
+}\
+.ace-cobalt .ace_fold {\
+background-color: #FF9D00;\
+border-color: #FFFFFF\
+}\
+.ace-cobalt .ace_support.ace_function {\
+color: #FFB054\
+}\
+.ace-cobalt .ace_storage {\
+color: #FFEE80\
+}\
+.ace-cobalt .ace_entity {\
+color: #FFDD00\
+}\
+.ace-cobalt .ace_string {\
+color: #3AD900\
+}\
+.ace-cobalt .ace_string.ace_regexp {\
+color: #80FFC2\
+}\
+.ace-cobalt .ace_comment {\
+font-style: italic;\
+color: #0088FF\
+}\
+.ace-cobalt .ace_heading,\
+.ace-cobalt .ace_markup.ace_heading {\
+color: #C8E4FD;\
+background-color: #001221\
+}\
+.ace-cobalt .ace_list,\
+.ace-cobalt .ace_markup.ace_list {\
+background-color: #130D26\
+}\
+.ace-cobalt .ace_variable {\
+color: #CCCCCC\
+}\
+.ace-cobalt .ace_variable.ace_language {\
+color: #FF80E1\
+}\
+.ace-cobalt .ace_meta.ace_tag {\
+color: #9EFFFF\
+}\
+.ace-cobalt .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHCLSvkPAAP3AgSDTRd4AAAAAElFTkSuQmCC) right repeat-y\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-crimson_editor.js b/services/web/public/js/ace-1.2.5/theme-crimson_editor.js
new file mode 100644
index 0000000000..a18855252b
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-crimson_editor.js
@@ -0,0 +1,118 @@
+ace.define("ace/theme/crimson_editor",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+exports.isDark = false;
+exports.cssText = ".ace-crimson-editor .ace_gutter {\
+background: #ebebeb;\
+color: #333;\
+overflow : hidden;\
+}\
+.ace-crimson-editor .ace_gutter-layer {\
+width: 100%;\
+text-align: right;\
+}\
+.ace-crimson-editor .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-crimson-editor {\
+background-color: #FFFFFF;\
+color: rgb(64, 64, 64);\
+}\
+.ace-crimson-editor .ace_cursor {\
+color: black;\
+}\
+.ace-crimson-editor .ace_invisible {\
+color: rgb(191, 191, 191);\
+}\
+.ace-crimson-editor .ace_identifier {\
+color: black;\
+}\
+.ace-crimson-editor .ace_keyword {\
+color: blue;\
+}\
+.ace-crimson-editor .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-crimson-editor .ace_constant.ace_language {\
+color: rgb(255, 156, 0);\
+}\
+.ace-crimson-editor .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-crimson-editor .ace_invalid {\
+text-decoration: line-through;\
+color: rgb(224, 0, 0);\
+}\
+.ace-crimson-editor .ace_fold {\
+}\
+.ace-crimson-editor .ace_support.ace_function {\
+color: rgb(192, 0, 0);\
+}\
+.ace-crimson-editor .ace_support.ace_constant {\
+color: rgb(6, 150, 14);\
+}\
+.ace-crimson-editor .ace_support.ace_type,\
+.ace-crimson-editor .ace_support.ace_class {\
+color: rgb(109, 121, 222);\
+}\
+.ace-crimson-editor .ace_keyword.ace_operator {\
+color: rgb(49, 132, 149);\
+}\
+.ace-crimson-editor .ace_string {\
+color: rgb(128, 0, 128);\
+}\
+.ace-crimson-editor .ace_comment {\
+color: rgb(76, 136, 107);\
+}\
+.ace-crimson-editor .ace_comment.ace_doc {\
+color: rgb(0, 102, 255);\
+}\
+.ace-crimson-editor .ace_comment.ace_doc.ace_tag {\
+color: rgb(128, 159, 191);\
+}\
+.ace-crimson-editor .ace_constant.ace_numeric {\
+color: rgb(0, 0, 64);\
+}\
+.ace-crimson-editor .ace_variable {\
+color: rgb(0, 64, 128);\
+}\
+.ace-crimson-editor .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_active-line {\
+background: rgb(232, 242, 254);\
+}\
+.ace-crimson-editor .ace_gutter-active-line {\
+background-color : #dcdcdc;\
+}\
+.ace-crimson-editor .ace_meta.ace_tag {\
+color:rgb(28, 2, 255);\
+}\
+.ace-crimson-editor .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-crimson-editor .ace_string.ace_regex {\
+color: rgb(192, 0, 192);\
+}\
+.ace-crimson-editor .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}";
+
+exports.cssClass = "ace-crimson-editor";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-dawn.js b/services/web/public/js/ace-1.2.5/theme-dawn.js
new file mode 100644
index 0000000000..f3c15c92ec
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-dawn.js
@@ -0,0 +1,108 @@
+ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-dawn";
+exports.cssText = ".ace-dawn .ace_gutter {\
+background: #ebebeb;\
+color: #333\
+}\
+.ace-dawn .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8\
+}\
+.ace-dawn {\
+background-color: #F9F9F9;\
+color: #080808\
+}\
+.ace-dawn .ace_cursor {\
+color: #000000\
+}\
+.ace-dawn .ace_marker-layer .ace_selection {\
+background: rgba(39, 95, 255, 0.30)\
+}\
+.ace-dawn.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #F9F9F9;\
+}\
+.ace-dawn .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0)\
+}\
+.ace-dawn .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(75, 75, 126, 0.50)\
+}\
+.ace-dawn .ace_marker-layer .ace_active-line {\
+background: rgba(36, 99, 180, 0.12)\
+}\
+.ace-dawn .ace_gutter-active-line {\
+background-color : #dcdcdc\
+}\
+.ace-dawn .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(39, 95, 255, 0.30)\
+}\
+.ace-dawn .ace_invisible {\
+color: rgba(75, 75, 126, 0.50)\
+}\
+.ace-dawn .ace_keyword,\
+.ace-dawn .ace_meta {\
+color: #794938\
+}\
+.ace-dawn .ace_constant,\
+.ace-dawn .ace_constant.ace_character,\
+.ace-dawn .ace_constant.ace_character.ace_escape,\
+.ace-dawn .ace_constant.ace_other {\
+color: #811F24\
+}\
+.ace-dawn .ace_invalid.ace_illegal {\
+text-decoration: underline;\
+font-style: italic;\
+color: #F8F8F8;\
+background-color: #B52A1D\
+}\
+.ace-dawn .ace_invalid.ace_deprecated {\
+text-decoration: underline;\
+font-style: italic;\
+color: #B52A1D\
+}\
+.ace-dawn .ace_support {\
+color: #691C97\
+}\
+.ace-dawn .ace_support.ace_constant {\
+color: #B4371F\
+}\
+.ace-dawn .ace_fold {\
+background-color: #794938;\
+border-color: #080808\
+}\
+.ace-dawn .ace_list,\
+.ace-dawn .ace_markup.ace_list,\
+.ace-dawn .ace_support.ace_function {\
+color: #693A17\
+}\
+.ace-dawn .ace_storage {\
+font-style: italic;\
+color: #A71D5D\
+}\
+.ace-dawn .ace_string {\
+color: #0B6125\
+}\
+.ace-dawn .ace_string.ace_regexp {\
+color: #CF5628\
+}\
+.ace-dawn .ace_comment {\
+font-style: italic;\
+color: #5A525F\
+}\
+.ace-dawn .ace_heading,\
+.ace-dawn .ace_markup.ace_heading {\
+color: #19356D\
+}\
+.ace-dawn .ace_variable {\
+color: #234A97\
+}\
+.ace-dawn .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-dreamweaver.js b/services/web/public/js/ace-1.2.5/theme-dreamweaver.js
new file mode 100644
index 0000000000..632b1ea9b0
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-dreamweaver.js
@@ -0,0 +1,141 @@
+ace.define("ace/theme/dreamweaver",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+exports.isDark = false;
+exports.cssClass = "ace-dreamweaver";
+exports.cssText = ".ace-dreamweaver .ace_gutter {\
+background: #e8e8e8;\
+color: #333;\
+}\
+.ace-dreamweaver .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-dreamweaver {\
+background-color: #FFFFFF;\
+color: black;\
+}\
+.ace-dreamweaver .ace_fold {\
+background-color: #757AD8;\
+}\
+.ace-dreamweaver .ace_cursor {\
+color: black;\
+}\
+.ace-dreamweaver .ace_invisible {\
+color: rgb(191, 191, 191);\
+}\
+.ace-dreamweaver .ace_storage,\
+.ace-dreamweaver .ace_keyword {\
+color: blue;\
+}\
+.ace-dreamweaver .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-dreamweaver .ace_constant.ace_language {\
+color: rgb(88, 92, 246);\
+}\
+.ace-dreamweaver .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-dreamweaver .ace_invalid {\
+background-color: rgb(153, 0, 0);\
+color: white;\
+}\
+.ace-dreamweaver .ace_support.ace_function {\
+color: rgb(60, 76, 114);\
+}\
+.ace-dreamweaver .ace_support.ace_constant {\
+color: rgb(6, 150, 14);\
+}\
+.ace-dreamweaver .ace_support.ace_type,\
+.ace-dreamweaver .ace_support.ace_class {\
+color: #009;\
+}\
+.ace-dreamweaver .ace_support.ace_php_tag {\
+color: #f00;\
+}\
+.ace-dreamweaver .ace_keyword.ace_operator {\
+color: rgb(104, 118, 135);\
+}\
+.ace-dreamweaver .ace_string {\
+color: #00F;\
+}\
+.ace-dreamweaver .ace_comment {\
+color: rgb(76, 136, 107);\
+}\
+.ace-dreamweaver .ace_comment.ace_doc {\
+color: rgb(0, 102, 255);\
+}\
+.ace-dreamweaver .ace_comment.ace_doc.ace_tag {\
+color: rgb(128, 159, 191);\
+}\
+.ace-dreamweaver .ace_constant.ace_numeric {\
+color: rgb(0, 0, 205);\
+}\
+.ace-dreamweaver .ace_variable {\
+color: #06F\
+}\
+.ace-dreamweaver .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-dreamweaver .ace_entity.ace_name.ace_function {\
+color: #00F;\
+}\
+.ace-dreamweaver .ace_heading {\
+color: rgb(12, 7, 255);\
+}\
+.ace-dreamweaver .ace_list {\
+color:rgb(185, 6, 144);\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.07);\
+}\
+.ace-dreamweaver .ace_gutter-active-line {\
+background-color : #DCDCDC;\
+}\
+.ace-dreamweaver .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-dreamweaver .ace_meta.ace_tag {\
+color:#009;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_anchor {\
+color:#060;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_form {\
+color:#F90;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_image {\
+color:#909;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_script {\
+color:#900;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_style {\
+color:#909;\
+}\
+.ace-dreamweaver .ace_meta.ace_tag.ace_table {\
+color:#099;\
+}\
+.ace-dreamweaver .ace_string.ace_regex {\
+color: rgb(255, 0, 0)\
+}\
+.ace-dreamweaver .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-eclipse.js b/services/web/public/js/ace-1.2.5/theme-eclipse.js
new file mode 100644
index 0000000000..63aa334cf3
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-eclipse.js
@@ -0,0 +1,98 @@
+ace.define("ace/theme/eclipse",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+"use strict";
+
+exports.isDark = false;
+exports.cssText = ".ace-eclipse .ace_gutter {\
+background: #ebebeb;\
+border-right: 1px solid rgb(159, 159, 159);\
+color: rgb(136, 136, 136);\
+}\
+.ace-eclipse .ace_print-margin {\
+width: 1px;\
+background: #ebebeb;\
+}\
+.ace-eclipse {\
+background-color: #FFFFFF;\
+color: black;\
+}\
+.ace-eclipse .ace_fold {\
+background-color: rgb(60, 76, 114);\
+}\
+.ace-eclipse .ace_cursor {\
+color: black;\
+}\
+.ace-eclipse .ace_storage,\
+.ace-eclipse .ace_keyword,\
+.ace-eclipse .ace_variable {\
+color: rgb(127, 0, 85);\
+}\
+.ace-eclipse .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-eclipse .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-eclipse .ace_function {\
+color: rgb(60, 76, 114);\
+}\
+.ace-eclipse .ace_string {\
+color: rgb(42, 0, 255);\
+}\
+.ace-eclipse .ace_comment {\
+color: rgb(113, 150, 130);\
+}\
+.ace-eclipse .ace_comment.ace_doc {\
+color: rgb(63, 95, 191);\
+}\
+.ace-eclipse .ace_comment.ace_doc.ace_tag {\
+color: rgb(127, 159, 191);\
+}\
+.ace-eclipse .ace_constant.ace_numeric {\
+color: darkblue;\
+}\
+.ace-eclipse .ace_tag {\
+color: rgb(25, 118, 116);\
+}\
+.ace-eclipse .ace_type {\
+color: rgb(127, 0, 127);\
+}\
+.ace-eclipse .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-eclipse .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-eclipse .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-eclipse .ace_meta.ace_tag {\
+color:rgb(25, 118, 116);\
+}\
+.ace-eclipse .ace_invisible {\
+color: #ddd;\
+}\
+.ace-eclipse .ace_entity.ace_other.ace_attribute-name {\
+color:rgb(127, 0, 127);\
+}\
+.ace-eclipse .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0);\
+}\
+.ace-eclipse .ace_active-line {\
+background: rgb(232, 242, 254);\
+}\
+.ace-eclipse .ace_gutter-active-line {\
+background-color : #DADADA;\
+}\
+.ace-eclipse .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgb(181, 213, 255);\
+}\
+.ace-eclipse .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}";
+
+exports.cssClass = "ace-eclipse";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-github.js b/services/web/public/js/ace-1.2.5/theme-github.js
new file mode 100644
index 0000000000..d19512c6ed
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-github.js
@@ -0,0 +1,103 @@
+ace.define("ace/theme/github",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-github";
+exports.cssText = "\
+.ace-github .ace_gutter {\
+background: #e8e8e8;\
+color: #AAA;\
+}\
+.ace-github {\
+background: #fff;\
+color: #000;\
+}\
+.ace-github .ace_keyword {\
+font-weight: bold;\
+}\
+.ace-github .ace_string {\
+color: #D14;\
+}\
+.ace-github .ace_variable.ace_class {\
+color: teal;\
+}\
+.ace-github .ace_constant.ace_numeric {\
+color: #099;\
+}\
+.ace-github .ace_constant.ace_buildin {\
+color: #0086B3;\
+}\
+.ace-github .ace_support.ace_function {\
+color: #0086B3;\
+}\
+.ace-github .ace_comment {\
+color: #998;\
+font-style: italic;\
+}\
+.ace-github .ace_variable.ace_language {\
+color: #0086B3;\
+}\
+.ace-github .ace_paren {\
+font-weight: bold;\
+}\
+.ace-github .ace_boolean {\
+font-weight: bold;\
+}\
+.ace-github .ace_string.ace_regexp {\
+color: #009926;\
+font-weight: normal;\
+}\
+.ace-github .ace_variable.ace_instance {\
+color: teal;\
+}\
+.ace-github .ace_constant.ace_language {\
+font-weight: bold;\
+}\
+.ace-github .ace_cursor {\
+color: black;\
+}\
+.ace-github.ace_focus .ace_marker-layer .ace_active-line {\
+background: rgb(255, 255, 204);\
+}\
+.ace-github .ace_marker-layer .ace_active-line {\
+background: rgb(245, 245, 245);\
+}\
+.ace-github .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-github.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px white;\
+}\
+.ace-github.ace_nobold .ace_line > span {\
+font-weight: normal !important;\
+}\
+.ace-github .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-github .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-github .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-github .ace_gutter-active-line {\
+background-color : rgba(0, 0, 0, 0.07);\
+}\
+.ace-github .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-github .ace_invisible {\
+color: #BFBFBF\
+}\
+.ace-github .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-github .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}";
+
+ var dom = require("../lib/dom");
+ dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-idle_fingers.js b/services/web/public/js/ace-1.2.5/theme-idle_fingers.js
new file mode 100644
index 0000000000..7fcf1cbdb4
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-idle_fingers.js
@@ -0,0 +1,96 @@
+ace.define("ace/theme/idle_fingers",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-idle-fingers";
+exports.cssText = ".ace-idle-fingers .ace_gutter {\
+background: #3b3b3b;\
+color: rgb(153,153,153)\
+}\
+.ace-idle-fingers .ace_print-margin {\
+width: 1px;\
+background: #3b3b3b\
+}\
+.ace-idle-fingers {\
+background-color: #323232;\
+color: #FFFFFF\
+}\
+.ace-idle-fingers .ace_cursor {\
+color: #91FF00\
+}\
+.ace-idle-fingers .ace_marker-layer .ace_selection {\
+background: rgba(90, 100, 126, 0.88)\
+}\
+.ace-idle-fingers.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #323232;\
+}\
+.ace-idle-fingers .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-idle-fingers .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #404040\
+}\
+.ace-idle-fingers .ace_marker-layer .ace_active-line {\
+background: #353637\
+}\
+.ace-idle-fingers .ace_gutter-active-line {\
+background-color: #353637\
+}\
+.ace-idle-fingers .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(90, 100, 126, 0.88)\
+}\
+.ace-idle-fingers .ace_invisible {\
+color: #404040\
+}\
+.ace-idle-fingers .ace_keyword,\
+.ace-idle-fingers .ace_meta {\
+color: #CC7833\
+}\
+.ace-idle-fingers .ace_constant,\
+.ace-idle-fingers .ace_constant.ace_character,\
+.ace-idle-fingers .ace_constant.ace_character.ace_escape,\
+.ace-idle-fingers .ace_constant.ace_other,\
+.ace-idle-fingers .ace_support.ace_constant {\
+color: #6C99BB\
+}\
+.ace-idle-fingers .ace_invalid {\
+color: #FFFFFF;\
+background-color: #FF0000\
+}\
+.ace-idle-fingers .ace_fold {\
+background-color: #CC7833;\
+border-color: #FFFFFF\
+}\
+.ace-idle-fingers .ace_support.ace_function {\
+color: #B83426\
+}\
+.ace-idle-fingers .ace_variable.ace_parameter {\
+font-style: italic\
+}\
+.ace-idle-fingers .ace_string {\
+color: #A5C261\
+}\
+.ace-idle-fingers .ace_string.ace_regexp {\
+color: #CCCC33\
+}\
+.ace-idle-fingers .ace_comment {\
+font-style: italic;\
+color: #BC9458\
+}\
+.ace-idle-fingers .ace_meta.ace_tag {\
+color: #FFE5BB\
+}\
+.ace-idle-fingers .ace_entity.ace_name {\
+color: #FFC66D\
+}\
+.ace-idle-fingers .ace_collab.ace_user1 {\
+color: #323232;\
+background-color: #FFF980\
+}\
+.ace-idle-fingers .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMwMjLyZYiPj/8PAAreAwAI1+g0AAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-iplastic.js b/services/web/public/js/ace-1.2.5/theme-iplastic.js
new file mode 100644
index 0000000000..593aa00edb
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-iplastic.js
@@ -0,0 +1,121 @@
+ace.define("ace/theme/iplastic",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-iplastic";
+exports.cssText = ".ace-iplastic .ace_gutter {\
+background: #dddddd;\
+color: #666666\
+}\
+.ace-iplastic .ace_print-margin {\
+width: 1px;\
+background: #bbbbbb\
+}\
+.ace-iplastic {\
+background-color: #eeeeee;\
+color: #333333\
+}\
+.ace-iplastic .ace_cursor {\
+color: #333\
+}\
+.ace-iplastic .ace_marker-layer .ace_selection {\
+background: #BAD6FD;\
+}\
+.ace-iplastic.ace_multiselect .ace_selection.ace_start {\
+border-radius: 4px\
+}\
+.ace-iplastic .ace_marker-layer .ace_step {\
+background: #444444\
+}\
+.ace-iplastic .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #49483E;\
+background: #FFF799\
+}\
+.ace-iplastic .ace_marker-layer .ace_active-line {\
+background: #e5e5e5\
+}\
+.ace-iplastic .ace_gutter-active-line {\
+background-color: #eeeeee\
+}\
+.ace-iplastic .ace_marker-layer .ace_selected-word {\
+border: 1px solid #555555;\
+border-radius:4px\
+}\
+.ace-iplastic .ace_invisible {\
+color: #999999\
+}\
+.ace-iplastic .ace_entity.ace_name.ace_tag,\
+.ace-iplastic .ace_keyword,\
+.ace-iplastic .ace_meta.ace_tag,\
+.ace-iplastic .ace_storage {\
+color: #0000FF\
+}\
+.ace-iplastic .ace_punctuation,\
+.ace-iplastic .ace_punctuation.ace_tag {\
+color: #000\
+}\
+.ace-iplastic .ace_constant {\
+color: #333333;\
+font-weight: 700\
+}\
+.ace-iplastic .ace_constant.ace_character,\
+.ace-iplastic .ace_constant.ace_language,\
+.ace-iplastic .ace_constant.ace_numeric,\
+.ace-iplastic .ace_constant.ace_other {\
+color: #0066FF;\
+font-weight: 700\
+}\
+.ace-iplastic .ace_constant.ace_numeric{\
+font-weight: 100\
+}\
+.ace-iplastic .ace_invalid {\
+color: #F8F8F0;\
+background-color: #F92672\
+}\
+.ace-iplastic .ace_invalid.ace_deprecated {\
+color: #F8F8F0;\
+background-color: #AE81FF\
+}\
+.ace-iplastic .ace_support.ace_constant,\
+.ace-iplastic .ace_support.ace_function {\
+color: #333333;\
+font-weight: 700\
+}\
+.ace-iplastic .ace_fold {\
+background-color: #464646;\
+border-color: #F8F8F2\
+}\
+.ace-iplastic .ace_storage.ace_type,\
+.ace-iplastic .ace_support.ace_class,\
+.ace-iplastic .ace_support.ace_type {\
+color: #3333fc;\
+font-weight: 700\
+}\
+.ace-iplastic .ace_entity.ace_name.ace_function,\
+.ace-iplastic .ace_entity.ace_other,\
+.ace-iplastic .ace_entity.ace_other.ace_attribute-name,\
+.ace-iplastic .ace_variable {\
+color: #3366cc;\
+font-style: italic\
+}\
+.ace-iplastic .ace_variable.ace_parameter {\
+font-style: italic;\
+color: #2469E0\
+}\
+.ace-iplastic .ace_string {\
+color: #a55f03\
+}\
+.ace-iplastic .ace_comment {\
+color: #777777;\
+font-style: italic\
+}\
+.ace-iplastic .ace_fold-widget {\
+background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==);\
+}\
+.ace-iplastic .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAABlJREFUeNpi+P//PwMzMzPzfwAAAAD//wMAGRsECSML/RIAAAAASUVORK5CYII=) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-katzenmilch.js b/services/web/public/js/ace-1.2.5/theme-katzenmilch.js
new file mode 100644
index 0000000000..f65ce4a81c
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-katzenmilch.js
@@ -0,0 +1,121 @@
+ace.define("ace/theme/katzenmilch",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-katzenmilch";
+exports.cssText = ".ace-katzenmilch .ace_gutter,\
+.ace-katzenmilch .ace_gutter {\
+background: #e8e8e8;\
+color: #333\
+}\
+.ace-katzenmilch .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8\
+}\
+.ace-katzenmilch {\
+background-color: #f3f2f3;\
+color: rgba(15, 0, 9, 1.0)\
+}\
+.ace-katzenmilch .ace_cursor {\
+border-left: 2px solid #100011\
+}\
+.ace-katzenmilch .ace_overwrite-cursors .ace_cursor {\
+border-left: 0px;\
+border-bottom: 1px solid #100011\
+}\
+.ace-katzenmilch .ace_marker-layer .ace_selection {\
+background: rgba(100, 5, 208, 0.27)\
+}\
+.ace-katzenmilch.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #f3f2f3;\
+}\
+.ace-katzenmilch .ace_marker-layer .ace_step {\
+background: rgb(198, 219, 174)\
+}\
+.ace-katzenmilch .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(0, 0, 0, 0.33);\
+}\
+.ace-katzenmilch .ace_marker-layer .ace_active-line {\
+background: rgb(232, 242, 254)\
+}\
+.ace-katzenmilch .ace_gutter-active-line {\
+background-color: rgb(232, 242, 254)\
+}\
+.ace-katzenmilch .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(100, 5, 208, 0.27)\
+}\
+.ace-katzenmilch .ace_invisible {\
+color: #BFBFBF\
+}\
+.ace-katzenmilch .ace_fold {\
+background-color: rgba(2, 95, 73, 0.97);\
+border-color: rgba(15, 0, 9, 1.0)\
+}\
+.ace-katzenmilch .ace_keyword {\
+color: #674Aa8;\
+rbackground-color: rgba(163, 170, 216, 0.055)\
+}\
+.ace-katzenmilch .ace_constant.ace_language {\
+color: #7D7e52;\
+rbackground-color: rgba(189, 190, 130, 0.059)\
+}\
+.ace-katzenmilch .ace_constant.ace_numeric {\
+color: rgba(79, 130, 123, 0.93);\
+rbackground-color: rgba(119, 194, 187, 0.059)\
+}\
+.ace-katzenmilch .ace_constant.ace_character,\
+.ace-katzenmilch .ace_constant.ace_other {\
+color: rgba(2, 95, 105, 1.0);\
+rbackground-color: rgba(127, 34, 153, 0.063)\
+}\
+.ace-katzenmilch .ace_support.ace_function {\
+color: #9D7e62;\
+rbackground-color: rgba(189, 190, 130, 0.039)\
+}\
+.ace-katzenmilch .ace_support.ace_class {\
+color: rgba(239, 106, 167, 1.0);\
+rbackground-color: rgba(239, 106, 167, 0.063)\
+}\
+.ace-katzenmilch .ace_storage {\
+color: rgba(123, 92, 191, 1.0);\
+rbackground-color: rgba(139, 93, 223, 0.051)\
+}\
+.ace-katzenmilch .ace_invalid {\
+color: #DFDFD5;\
+rbackground-color: #CC1B27\
+}\
+.ace-katzenmilch .ace_string {\
+color: #5a5f9b;\
+rbackground-color: rgba(170, 175, 219, 0.035)\
+}\
+.ace-katzenmilch .ace_comment {\
+font-style: italic;\
+color: rgba(64, 79, 80, 0.67);\
+rbackground-color: rgba(95, 15, 255, 0.0078)\
+}\
+.ace-katzenmilch .ace_entity.ace_name.ace_function,\
+.ace-katzenmilch .ace_variable {\
+color: rgba(2, 95, 73, 0.97);\
+rbackground-color: rgba(34, 255, 73, 0.12)\
+}\
+.ace-katzenmilch .ace_variable.ace_language {\
+color: #316fcf;\
+rbackground-color: rgba(58, 175, 255, 0.039)\
+}\
+.ace-katzenmilch .ace_variable.ace_parameter {\
+font-style: italic;\
+color: rgba(51, 150, 159, 0.87);\
+rbackground-color: rgba(5, 214, 249, 0.043)\
+}\
+.ace-katzenmilch .ace_entity.ace_other.ace_attribute-name {\
+color: rgba(73, 70, 194, 0.93);\
+rbackground-color: rgba(73, 134, 194, 0.035)\
+}\
+.ace-katzenmilch .ace_entity.ace_name.ace_tag {\
+color: #3976a2;\
+rbackground-color: rgba(73, 166, 210, 0.039)\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-kr_theme.js b/services/web/public/js/ace-1.2.5/theme-kr_theme.js
new file mode 100644
index 0000000000..8818b33e76
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-kr_theme.js
@@ -0,0 +1,104 @@
+ace.define("ace/theme/kr_theme",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-kr-theme";
+exports.cssText = ".ace-kr-theme .ace_gutter {\
+background: #1c1917;\
+color: #FCFFE0\
+}\
+.ace-kr-theme .ace_print-margin {\
+width: 1px;\
+background: #1c1917\
+}\
+.ace-kr-theme {\
+background-color: #0B0A09;\
+color: #FCFFE0\
+}\
+.ace-kr-theme .ace_cursor {\
+color: #FF9900\
+}\
+.ace-kr-theme .ace_marker-layer .ace_selection {\
+background: rgba(170, 0, 255, 0.45)\
+}\
+.ace-kr-theme.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #0B0A09;\
+}\
+.ace-kr-theme .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-kr-theme .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(255, 177, 111, 0.32)\
+}\
+.ace-kr-theme .ace_marker-layer .ace_active-line {\
+background: #38403D\
+}\
+.ace-kr-theme .ace_gutter-active-line {\
+background-color : #38403D\
+}\
+.ace-kr-theme .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(170, 0, 255, 0.45)\
+}\
+.ace-kr-theme .ace_invisible {\
+color: rgba(255, 177, 111, 0.32)\
+}\
+.ace-kr-theme .ace_keyword,\
+.ace-kr-theme .ace_meta {\
+color: #949C8B\
+}\
+.ace-kr-theme .ace_constant,\
+.ace-kr-theme .ace_constant.ace_character,\
+.ace-kr-theme .ace_constant.ace_character.ace_escape,\
+.ace-kr-theme .ace_constant.ace_other {\
+color: rgba(210, 117, 24, 0.76)\
+}\
+.ace-kr-theme .ace_invalid {\
+color: #F8F8F8;\
+background-color: #A41300\
+}\
+.ace-kr-theme .ace_support {\
+color: #9FC28A\
+}\
+.ace-kr-theme .ace_support.ace_constant {\
+color: #C27E66\
+}\
+.ace-kr-theme .ace_fold {\
+background-color: #949C8B;\
+border-color: #FCFFE0\
+}\
+.ace-kr-theme .ace_support.ace_function {\
+color: #85873A\
+}\
+.ace-kr-theme .ace_storage {\
+color: #FFEE80\
+}\
+.ace-kr-theme .ace_string {\
+color: rgba(164, 161, 181, 0.8)\
+}\
+.ace-kr-theme .ace_string.ace_regexp {\
+color: rgba(125, 255, 192, 0.65)\
+}\
+.ace-kr-theme .ace_comment {\
+font-style: italic;\
+color: #706D5B\
+}\
+.ace-kr-theme .ace_variable {\
+color: #D1A796\
+}\
+.ace-kr-theme .ace_list,\
+.ace-kr-theme .ace_markup.ace_list {\
+background-color: #0F0040\
+}\
+.ace-kr-theme .ace_variable.ace_language {\
+color: #FF80E1\
+}\
+.ace-kr-theme .ace_meta.ace_tag {\
+color: #BABD9C\
+}\
+.ace-kr-theme .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYFBXV/8PAAJoAXX4kT2EAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-kuroir.js b/services/web/public/js/ace-1.2.5/theme-kuroir.js
new file mode 100644
index 0000000000..30e0a8bb38
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-kuroir.js
@@ -0,0 +1,61 @@
+ace.define("ace/theme/kuroir",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-kuroir";
+exports.cssText = "\
+.ace-kuroir .ace_gutter {\
+background: #e8e8e8;\
+color: #333;\
+}\
+.ace-kuroir .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-kuroir {\
+background-color: #E8E9E8;\
+color: #363636;\
+}\
+.ace-kuroir .ace_cursor {\
+color: #202020;\
+}\
+.ace-kuroir .ace_marker-layer .ace_selection {\
+background: rgba(245, 170, 0, 0.57);\
+}\
+.ace-kuroir.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #E8E9E8;\
+}\
+.ace-kuroir .ace_marker-layer .ace_step {\
+background: rgb(198, 219, 174);\
+}\
+.ace-kuroir .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(0, 0, 0, 0.29);\
+}\
+.ace-kuroir .ace_marker-layer .ace_active-line {\
+background: rgba(203, 220, 47, 0.22);\
+}\
+.ace-kuroir .ace_gutter-active-line {\
+background-color: rgba(203, 220, 47, 0.22);\
+}\
+.ace-kuroir .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(245, 170, 0, 0.57);\
+}\
+.ace-kuroir .ace_invisible {\
+color: #BFBFBF\
+}\
+.ace-kuroir .ace_fold {\
+border-color: #363636;\
+}\
+.ace-kuroir .ace_constant{color:#CD6839;}.ace-kuroir .ace_constant.ace_numeric{color:#9A5925;}.ace-kuroir .ace_support{color:#104E8B;}.ace-kuroir .ace_support.ace_function{color:#005273;}.ace-kuroir .ace_support.ace_constant{color:#CF6A4C;}.ace-kuroir .ace_storage{color:#A52A2A;}.ace-kuroir .ace_invalid.ace_illegal{color:#FD1224;\
+background-color:rgba(255, 6, 0, 0.15);}.ace-kuroir .ace_invalid.ace_deprecated{text-decoration:underline;\
+font-style:italic;\
+color:#FD1732;\
+background-color:#E8E9E8;}.ace-kuroir .ace_string{color:#639300;}.ace-kuroir .ace_string.ace_regexp{color:#417E00;\
+background-color:#C9D4BE;}.ace-kuroir .ace_comment{color:rgba(148, 148, 148, 0.91);\
+background-color:rgba(220, 220, 220, 0.56);}.ace-kuroir .ace_variable{color:#009ACD;}.ace-kuroir .ace_meta.ace_tag{color:#005273;}.ace-kuroir .ace_markup.ace_heading{color:#B8012D;\
+background-color:rgba(191, 97, 51, 0.051);}.ace-kuroir .ace_markup.ace_list{color:#8F5B26;}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-merbivore.js b/services/web/public/js/ace-1.2.5/theme-merbivore.js
new file mode 100644
index 0000000000..fc0a72f1cc
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-merbivore.js
@@ -0,0 +1,95 @@
+ace.define("ace/theme/merbivore",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-merbivore";
+exports.cssText = ".ace-merbivore .ace_gutter {\
+background: #202020;\
+color: #E6E1DC\
+}\
+.ace-merbivore .ace_print-margin {\
+width: 1px;\
+background: #555651\
+}\
+.ace-merbivore {\
+background-color: #161616;\
+color: #E6E1DC\
+}\
+.ace-merbivore .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-merbivore .ace_marker-layer .ace_selection {\
+background: #454545\
+}\
+.ace-merbivore.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #161616;\
+}\
+.ace-merbivore .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-merbivore .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #404040\
+}\
+.ace-merbivore .ace_marker-layer .ace_active-line {\
+background: #333435\
+}\
+.ace-merbivore .ace_gutter-active-line {\
+background-color: #333435\
+}\
+.ace-merbivore .ace_marker-layer .ace_selected-word {\
+border: 1px solid #454545\
+}\
+.ace-merbivore .ace_invisible {\
+color: #404040\
+}\
+.ace-merbivore .ace_entity.ace_name.ace_tag,\
+.ace-merbivore .ace_keyword,\
+.ace-merbivore .ace_meta,\
+.ace-merbivore .ace_meta.ace_tag,\
+.ace-merbivore .ace_storage,\
+.ace-merbivore .ace_support.ace_function {\
+color: #FC6F09\
+}\
+.ace-merbivore .ace_constant,\
+.ace-merbivore .ace_constant.ace_character,\
+.ace-merbivore .ace_constant.ace_character.ace_escape,\
+.ace-merbivore .ace_constant.ace_other,\
+.ace-merbivore .ace_support.ace_type {\
+color: #1EDAFB\
+}\
+.ace-merbivore .ace_constant.ace_character.ace_escape {\
+color: #519F50\
+}\
+.ace-merbivore .ace_constant.ace_language {\
+color: #FDC251\
+}\
+.ace-merbivore .ace_constant.ace_library,\
+.ace-merbivore .ace_string,\
+.ace-merbivore .ace_support.ace_constant {\
+color: #8DFF0A\
+}\
+.ace-merbivore .ace_constant.ace_numeric {\
+color: #58C554\
+}\
+.ace-merbivore .ace_invalid {\
+color: #FFFFFF;\
+background-color: #990000\
+}\
+.ace-merbivore .ace_fold {\
+background-color: #FC6F09;\
+border-color: #E6E1DC\
+}\
+.ace-merbivore .ace_comment {\
+font-style: italic;\
+color: #AD2EA4\
+}\
+.ace-merbivore .ace_entity.ace_other.ace_attribute-name {\
+color: #FFFF89\
+}\
+.ace-merbivore .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMQFxf3ZXB1df0PAAdsAmERTkEHAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-merbivore_soft.js b/services/web/public/js/ace-1.2.5/theme-merbivore_soft.js
new file mode 100644
index 0000000000..eff2464651
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-merbivore_soft.js
@@ -0,0 +1,96 @@
+ace.define("ace/theme/merbivore_soft",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-merbivore-soft";
+exports.cssText = ".ace-merbivore-soft .ace_gutter {\
+background: #262424;\
+color: #E6E1DC\
+}\
+.ace-merbivore-soft .ace_print-margin {\
+width: 1px;\
+background: #262424\
+}\
+.ace-merbivore-soft {\
+background-color: #1C1C1C;\
+color: #E6E1DC\
+}\
+.ace-merbivore-soft .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-merbivore-soft .ace_marker-layer .ace_selection {\
+background: #494949\
+}\
+.ace-merbivore-soft.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #1C1C1C;\
+}\
+.ace-merbivore-soft .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-merbivore-soft .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #404040\
+}\
+.ace-merbivore-soft .ace_marker-layer .ace_active-line {\
+background: #333435\
+}\
+.ace-merbivore-soft .ace_gutter-active-line {\
+background-color: #333435\
+}\
+.ace-merbivore-soft .ace_marker-layer .ace_selected-word {\
+border: 1px solid #494949\
+}\
+.ace-merbivore-soft .ace_invisible {\
+color: #404040\
+}\
+.ace-merbivore-soft .ace_entity.ace_name.ace_tag,\
+.ace-merbivore-soft .ace_keyword,\
+.ace-merbivore-soft .ace_meta,\
+.ace-merbivore-soft .ace_meta.ace_tag,\
+.ace-merbivore-soft .ace_storage {\
+color: #FC803A\
+}\
+.ace-merbivore-soft .ace_constant,\
+.ace-merbivore-soft .ace_constant.ace_character,\
+.ace-merbivore-soft .ace_constant.ace_character.ace_escape,\
+.ace-merbivore-soft .ace_constant.ace_other,\
+.ace-merbivore-soft .ace_support.ace_type {\
+color: #68C1D8\
+}\
+.ace-merbivore-soft .ace_constant.ace_character.ace_escape {\
+color: #B3E5B4\
+}\
+.ace-merbivore-soft .ace_constant.ace_language {\
+color: #E1C582\
+}\
+.ace-merbivore-soft .ace_constant.ace_library,\
+.ace-merbivore-soft .ace_string,\
+.ace-merbivore-soft .ace_support.ace_constant {\
+color: #8EC65F\
+}\
+.ace-merbivore-soft .ace_constant.ace_numeric {\
+color: #7FC578\
+}\
+.ace-merbivore-soft .ace_invalid,\
+.ace-merbivore-soft .ace_invalid.ace_deprecated {\
+color: #FFFFFF;\
+background-color: #FE3838\
+}\
+.ace-merbivore-soft .ace_fold {\
+background-color: #FC803A;\
+border-color: #E6E1DC\
+}\
+.ace-merbivore-soft .ace_comment,\
+.ace-merbivore-soft .ace_meta {\
+font-style: italic;\
+color: #AC4BB8\
+}\
+.ace-merbivore-soft .ace_entity.ace_other.ace_attribute-name {\
+color: #EAF1A3\
+}\
+.ace-merbivore-soft .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWOQkpLyZfD09PwPAAfYAnaStpHRAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-mono_industrial.js b/services/web/public/js/ace-1.2.5/theme-mono_industrial.js
new file mode 100644
index 0000000000..0ece0309cb
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-mono_industrial.js
@@ -0,0 +1,107 @@
+ace.define("ace/theme/mono_industrial",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-mono-industrial";
+exports.cssText = ".ace-mono-industrial .ace_gutter {\
+background: #1d2521;\
+color: #C5C9C9\
+}\
+.ace-mono-industrial .ace_print-margin {\
+width: 1px;\
+background: #555651\
+}\
+.ace-mono-industrial {\
+background-color: #222C28;\
+color: #FFFFFF\
+}\
+.ace-mono-industrial .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-mono-industrial .ace_marker-layer .ace_selection {\
+background: rgba(145, 153, 148, 0.40)\
+}\
+.ace-mono-industrial.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #222C28;\
+}\
+.ace-mono-industrial .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-mono-industrial .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(102, 108, 104, 0.50)\
+}\
+.ace-mono-industrial .ace_marker-layer .ace_active-line {\
+background: rgba(12, 13, 12, 0.25)\
+}\
+.ace-mono-industrial .ace_gutter-active-line {\
+background-color: rgba(12, 13, 12, 0.25)\
+}\
+.ace-mono-industrial .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(145, 153, 148, 0.40)\
+}\
+.ace-mono-industrial .ace_invisible {\
+color: rgba(102, 108, 104, 0.50)\
+}\
+.ace-mono-industrial .ace_string {\
+background-color: #151C19;\
+color: #FFFFFF\
+}\
+.ace-mono-industrial .ace_keyword,\
+.ace-mono-industrial .ace_meta {\
+color: #A39E64\
+}\
+.ace-mono-industrial .ace_constant,\
+.ace-mono-industrial .ace_constant.ace_character,\
+.ace-mono-industrial .ace_constant.ace_character.ace_escape,\
+.ace-mono-industrial .ace_constant.ace_numeric,\
+.ace-mono-industrial .ace_constant.ace_other {\
+color: #E98800\
+}\
+.ace-mono-industrial .ace_entity.ace_name.ace_function,\
+.ace-mono-industrial .ace_keyword.ace_operator,\
+.ace-mono-industrial .ace_variable {\
+color: #A8B3AB\
+}\
+.ace-mono-industrial .ace_invalid {\
+color: #FFFFFF;\
+background-color: rgba(153, 0, 0, 0.68)\
+}\
+.ace-mono-industrial .ace_support.ace_constant {\
+color: #C87500\
+}\
+.ace-mono-industrial .ace_fold {\
+background-color: #A8B3AB;\
+border-color: #FFFFFF\
+}\
+.ace-mono-industrial .ace_support.ace_function {\
+color: #588E60\
+}\
+.ace-mono-industrial .ace_entity.ace_name,\
+.ace-mono-industrial .ace_support.ace_class,\
+.ace-mono-industrial .ace_support.ace_type {\
+color: #5778B6\
+}\
+.ace-mono-industrial .ace_storage {\
+color: #C23B00\
+}\
+.ace-mono-industrial .ace_variable.ace_language,\
+.ace-mono-industrial .ace_variable.ace_parameter {\
+color: #648BD2\
+}\
+.ace-mono-industrial .ace_comment {\
+color: #666C68;\
+background-color: #151C19\
+}\
+.ace-mono-industrial .ace_entity.ace_other.ace_attribute-name {\
+color: #909993\
+}\
+.ace-mono-industrial .ace_entity.ace_name.ace_tag {\
+color: #A65EFF\
+}\
+.ace-mono-industrial .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNQ1NbwZfALD/4PAAlTArlEC4r/AAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-monokai.js b/services/web/public/js/ace-1.2.5/theme-monokai.js
new file mode 100644
index 0000000000..322c2fa88d
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-monokai.js
@@ -0,0 +1,105 @@
+ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-monokai";
+exports.cssText = ".ace-monokai .ace_gutter {\
+background: #2F3129;\
+color: #8F908A\
+}\
+.ace-monokai .ace_print-margin {\
+width: 1px;\
+background: #555651\
+}\
+.ace-monokai {\
+background-color: #272822;\
+color: #F8F8F2\
+}\
+.ace-monokai .ace_cursor {\
+color: #F8F8F0\
+}\
+.ace-monokai .ace_marker-layer .ace_selection {\
+background: #49483E\
+}\
+.ace-monokai.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #272822;\
+}\
+.ace-monokai .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-monokai .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #49483E\
+}\
+.ace-monokai .ace_marker-layer .ace_active-line {\
+background: #202020\
+}\
+.ace-monokai .ace_gutter-active-line {\
+background-color: #272727\
+}\
+.ace-monokai .ace_marker-layer .ace_selected-word {\
+border: 1px solid #49483E\
+}\
+.ace-monokai .ace_invisible {\
+color: #52524d\
+}\
+.ace-monokai .ace_entity.ace_name.ace_tag,\
+.ace-monokai .ace_keyword,\
+.ace-monokai .ace_meta.ace_tag,\
+.ace-monokai .ace_storage {\
+color: #F92672\
+}\
+.ace-monokai .ace_punctuation,\
+.ace-monokai .ace_punctuation.ace_tag {\
+color: #fff\
+}\
+.ace-monokai .ace_constant.ace_character,\
+.ace-monokai .ace_constant.ace_language,\
+.ace-monokai .ace_constant.ace_numeric,\
+.ace-monokai .ace_constant.ace_other {\
+color: #AE81FF\
+}\
+.ace-monokai .ace_invalid {\
+color: #F8F8F0;\
+background-color: #F92672\
+}\
+.ace-monokai .ace_invalid.ace_deprecated {\
+color: #F8F8F0;\
+background-color: #AE81FF\
+}\
+.ace-monokai .ace_support.ace_constant,\
+.ace-monokai .ace_support.ace_function {\
+color: #66D9EF\
+}\
+.ace-monokai .ace_fold {\
+background-color: #A6E22E;\
+border-color: #F8F8F2\
+}\
+.ace-monokai .ace_storage.ace_type,\
+.ace-monokai .ace_support.ace_class,\
+.ace-monokai .ace_support.ace_type {\
+font-style: italic;\
+color: #66D9EF\
+}\
+.ace-monokai .ace_entity.ace_name.ace_function,\
+.ace-monokai .ace_entity.ace_other,\
+.ace-monokai .ace_entity.ace_other.ace_attribute-name,\
+.ace-monokai .ace_variable {\
+color: #A6E22E\
+}\
+.ace-monokai .ace_variable.ace_parameter {\
+font-style: italic;\
+color: #FD971F\
+}\
+.ace-monokai .ace_string {\
+color: #E6DB74\
+}\
+.ace-monokai .ace_comment {\
+color: #75715E\
+}\
+.ace-monokai .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-pastel_on_dark.js b/services/web/public/js/ace-1.2.5/theme-pastel_on_dark.js
new file mode 100644
index 0000000000..2631ae0035
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-pastel_on_dark.js
@@ -0,0 +1,108 @@
+ace.define("ace/theme/pastel_on_dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-pastel-on-dark";
+exports.cssText = ".ace-pastel-on-dark .ace_gutter {\
+background: #353030;\
+color: #8F938F\
+}\
+.ace-pastel-on-dark .ace_print-margin {\
+width: 1px;\
+background: #353030\
+}\
+.ace-pastel-on-dark {\
+background-color: #2C2828;\
+color: #8F938F\
+}\
+.ace-pastel-on-dark .ace_cursor {\
+color: #A7A7A7\
+}\
+.ace-pastel-on-dark .ace_marker-layer .ace_selection {\
+background: rgba(221, 240, 255, 0.20)\
+}\
+.ace-pastel-on-dark.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #2C2828;\
+}\
+.ace-pastel-on-dark .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-pastel-on-dark .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(255, 255, 255, 0.25)\
+}\
+.ace-pastel-on-dark .ace_marker-layer .ace_active-line {\
+background: rgba(255, 255, 255, 0.031)\
+}\
+.ace-pastel-on-dark .ace_gutter-active-line {\
+background-color: rgba(255, 255, 255, 0.031)\
+}\
+.ace-pastel-on-dark .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(221, 240, 255, 0.20)\
+}\
+.ace-pastel-on-dark .ace_invisible {\
+color: rgba(255, 255, 255, 0.25)\
+}\
+.ace-pastel-on-dark .ace_keyword,\
+.ace-pastel-on-dark .ace_meta {\
+color: #757aD8\
+}\
+.ace-pastel-on-dark .ace_constant,\
+.ace-pastel-on-dark .ace_constant.ace_character,\
+.ace-pastel-on-dark .ace_constant.ace_character.ace_escape,\
+.ace-pastel-on-dark .ace_constant.ace_other {\
+color: #4FB7C5\
+}\
+.ace-pastel-on-dark .ace_keyword.ace_operator {\
+color: #797878\
+}\
+.ace-pastel-on-dark .ace_constant.ace_character {\
+color: #AFA472\
+}\
+.ace-pastel-on-dark .ace_constant.ace_language {\
+color: #DE8E30\
+}\
+.ace-pastel-on-dark .ace_constant.ace_numeric {\
+color: #CCCCCC\
+}\
+.ace-pastel-on-dark .ace_invalid,\
+.ace-pastel-on-dark .ace_invalid.ace_illegal {\
+color: #F8F8F8;\
+background-color: rgba(86, 45, 86, 0.75)\
+}\
+.ace-pastel-on-dark .ace_invalid.ace_deprecated {\
+text-decoration: underline;\
+font-style: italic;\
+color: #D2A8A1\
+}\
+.ace-pastel-on-dark .ace_fold {\
+background-color: #757aD8;\
+border-color: #8F938F\
+}\
+.ace-pastel-on-dark .ace_support.ace_function {\
+color: #AEB2F8\
+}\
+.ace-pastel-on-dark .ace_string {\
+color: #66A968\
+}\
+.ace-pastel-on-dark .ace_string.ace_regexp {\
+color: #E9C062\
+}\
+.ace-pastel-on-dark .ace_comment {\
+color: #A6C6FF\
+}\
+.ace-pastel-on-dark .ace_variable {\
+color: #BEBF55\
+}\
+.ace-pastel-on-dark .ace_variable.ace_language {\
+color: #C1C144\
+}\
+.ace-pastel-on-dark .ace_xml-pe {\
+color: #494949\
+}\
+.ace-pastel-on-dark .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYIiPj/8PAARgAh2NTMh8AAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-solarized_dark.js b/services/web/public/js/ace-1.2.5/theme-solarized_dark.js
new file mode 100644
index 0000000000..d1acdb46ad
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-solarized_dark.js
@@ -0,0 +1,88 @@
+ace.define("ace/theme/solarized_dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-solarized-dark";
+exports.cssText = ".ace-solarized-dark .ace_gutter {\
+background: #01313f;\
+color: #d0edf7\
+}\
+.ace-solarized-dark .ace_print-margin {\
+width: 1px;\
+background: #33555E\
+}\
+.ace-solarized-dark {\
+background-color: #002B36;\
+color: #93A1A1\
+}\
+.ace-solarized-dark .ace_entity.ace_other.ace_attribute-name,\
+.ace-solarized-dark .ace_storage {\
+color: #93A1A1\
+}\
+.ace-solarized-dark .ace_cursor,\
+.ace-solarized-dark .ace_string.ace_regexp {\
+color: #D30102\
+}\
+.ace-solarized-dark .ace_marker-layer .ace_active-line,\
+.ace-solarized-dark .ace_marker-layer .ace_selection {\
+background: rgba(255, 255, 255, 0.1)\
+}\
+.ace-solarized-dark.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #002B36;\
+}\
+.ace-solarized-dark .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-solarized-dark .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(147, 161, 161, 0.50)\
+}\
+.ace-solarized-dark .ace_gutter-active-line {\
+background-color: #0d3440\
+}\
+.ace-solarized-dark .ace_marker-layer .ace_selected-word {\
+border: 1px solid #073642\
+}\
+.ace-solarized-dark .ace_invisible {\
+color: rgba(147, 161, 161, 0.50)\
+}\
+.ace-solarized-dark .ace_keyword,\
+.ace-solarized-dark .ace_meta,\
+.ace-solarized-dark .ace_support.ace_class,\
+.ace-solarized-dark .ace_support.ace_type {\
+color: #859900\
+}\
+.ace-solarized-dark .ace_constant.ace_character,\
+.ace-solarized-dark .ace_constant.ace_other {\
+color: #CB4B16\
+}\
+.ace-solarized-dark .ace_constant.ace_language {\
+color: #B58900\
+}\
+.ace-solarized-dark .ace_constant.ace_numeric {\
+color: #D33682\
+}\
+.ace-solarized-dark .ace_fold {\
+background-color: #268BD2;\
+border-color: #93A1A1\
+}\
+.ace-solarized-dark .ace_entity.ace_name.ace_function,\
+.ace-solarized-dark .ace_entity.ace_name.ace_tag,\
+.ace-solarized-dark .ace_support.ace_function,\
+.ace-solarized-dark .ace_variable,\
+.ace-solarized-dark .ace_variable.ace_language {\
+color: #268BD2\
+}\
+.ace-solarized-dark .ace_string {\
+color: #2AA198\
+}\
+.ace-solarized-dark .ace_comment {\
+font-style: italic;\
+color: #657B83\
+}\
+.ace-solarized-dark .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNg0Db1ZVCxc/sPAAd4AlUHlLenAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-solarized_light.js b/services/web/public/js/ace-1.2.5/theme-solarized_light.js
new file mode 100644
index 0000000000..f0c078ae5d
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-solarized_light.js
@@ -0,0 +1,91 @@
+ace.define("ace/theme/solarized_light",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-solarized-light";
+exports.cssText = ".ace-solarized-light .ace_gutter {\
+background: #fbf1d3;\
+color: #333\
+}\
+.ace-solarized-light .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8\
+}\
+.ace-solarized-light {\
+background-color: #FDF6E3;\
+color: #586E75\
+}\
+.ace-solarized-light .ace_cursor {\
+color: #000000\
+}\
+.ace-solarized-light .ace_marker-layer .ace_selection {\
+background: rgba(7, 54, 67, 0.09)\
+}\
+.ace-solarized-light.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #FDF6E3;\
+}\
+.ace-solarized-light .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0)\
+}\
+.ace-solarized-light .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(147, 161, 161, 0.50)\
+}\
+.ace-solarized-light .ace_marker-layer .ace_active-line {\
+background: #EEE8D5\
+}\
+.ace-solarized-light .ace_gutter-active-line {\
+background-color : #EDE5C1\
+}\
+.ace-solarized-light .ace_marker-layer .ace_selected-word {\
+border: 1px solid #073642\
+}\
+.ace-solarized-light .ace_invisible {\
+color: rgba(147, 161, 161, 0.50)\
+}\
+.ace-solarized-light .ace_keyword,\
+.ace-solarized-light .ace_meta,\
+.ace-solarized-light .ace_support.ace_class,\
+.ace-solarized-light .ace_support.ace_type {\
+color: #859900\
+}\
+.ace-solarized-light .ace_constant.ace_character,\
+.ace-solarized-light .ace_constant.ace_other {\
+color: #CB4B16\
+}\
+.ace-solarized-light .ace_constant.ace_language {\
+color: #B58900\
+}\
+.ace-solarized-light .ace_constant.ace_numeric {\
+color: #D33682\
+}\
+.ace-solarized-light .ace_fold {\
+background-color: #268BD2;\
+border-color: #586E75\
+}\
+.ace-solarized-light .ace_entity.ace_name.ace_function,\
+.ace-solarized-light .ace_entity.ace_name.ace_tag,\
+.ace-solarized-light .ace_support.ace_function,\
+.ace-solarized-light .ace_variable,\
+.ace-solarized-light .ace_variable.ace_language {\
+color: #268BD2\
+}\
+.ace-solarized-light .ace_storage {\
+color: #073642\
+}\
+.ace-solarized-light .ace_string {\
+color: #2AA198\
+}\
+.ace-solarized-light .ace_string.ace_regexp {\
+color: #D30102\
+}\
+.ace-solarized-light .ace_comment,\
+.ace-solarized-light .ace_entity.ace_other.ace_attribute-name {\
+color: #93A1A1\
+}\
+.ace-solarized-light .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHjy8NJ/AAjgA5fzQUmBAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-sqlserver.js b/services/web/public/js/ace-1.2.5/theme-sqlserver.js
new file mode 100644
index 0000000000..91f34f6c40
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-sqlserver.js
@@ -0,0 +1,138 @@
+ace.define("ace/theme/sqlserver",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-sqlserver";
+exports.cssText = ".ace-sqlserver .ace_gutter {\
+background: #ebebeb;\
+color: #333;\
+overflow: hidden;\
+}\
+.ace-sqlserver .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-sqlserver {\
+background-color: #FFFFFF;\
+color: black;\
+}\
+.ace-sqlserver .ace_identifier {\
+color: black;\
+}\
+.ace-sqlserver .ace_keyword {\
+color: #0000FF;\
+}\
+.ace-sqlserver .ace_numeric {\
+color: black;\
+}\
+.ace-sqlserver .ace_storage {\
+color: #11B7BE;\
+}\
+.ace-sqlserver .ace_keyword.ace_operator,\
+.ace-sqlserver .ace_lparen,\
+.ace-sqlserver .ace_rparen,\
+.ace-sqlserver .ace_punctuation {\
+color: #808080;\
+}\
+.ace-sqlserver .ace_set.ace_statement {\
+color: #0000FF;\
+text-decoration: underline;\
+}\
+.ace-sqlserver .ace_cursor {\
+color: black;\
+}\
+.ace-sqlserver .ace_invisible {\
+color: rgb(191, 191, 191);\
+}\
+.ace-sqlserver .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-sqlserver .ace_constant.ace_language {\
+color: #979797;\
+}\
+.ace-sqlserver .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-sqlserver .ace_invalid {\
+background-color: rgb(153, 0, 0);\
+color: white;\
+}\
+.ace-sqlserver .ace_support.ace_function {\
+color: #FF00FF;\
+}\
+.ace-sqlserver .ace_support.ace_constant {\
+color: rgb(6, 150, 14);\
+}\
+.ace-sqlserver .ace_class {\
+color: #008080;\
+}\
+.ace-sqlserver .ace_support.ace_other {\
+color: #6D79DE;\
+}\
+.ace-sqlserver .ace_variable.ace_parameter {\
+font-style: italic;\
+color: #FD971F;\
+}\
+.ace-sqlserver .ace_comment {\
+color: #008000;\
+}\
+.ace-sqlserver .ace_constant.ace_numeric {\
+color: black;\
+}\
+.ace-sqlserver .ace_variable {\
+color: rgb(49, 132, 149);\
+}\
+.ace-sqlserver .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-sqlserver .ace_support.ace_storedprocedure {\
+color: #800000;\
+}\
+.ace-sqlserver .ace_heading {\
+color: rgb(12, 7, 255);\
+}\
+.ace-sqlserver .ace_list {\
+color: rgb(185, 6, 144);\
+}\
+.ace-sqlserver .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-sqlserver .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-sqlserver .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-sqlserver .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-sqlserver .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.07);\
+}\
+.ace-sqlserver .ace_gutter-active-line {\
+background-color: #dcdcdc;\
+}\
+.ace-sqlserver .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-sqlserver .ace_meta.ace_tag {\
+color: #0000FF;\
+}\
+.ace-sqlserver .ace_string.ace_regex {\
+color: #FF0000;\
+}\
+.ace-sqlserver .ace_string {\
+color: #FF0000;\
+}\
+.ace-sqlserver .ace_entity.ace_other.ace_attribute-name {\
+color: #994409;\
+}\
+.ace-sqlserver .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-terminal.js b/services/web/public/js/ace-1.2.5/theme-terminal.js
new file mode 100644
index 0000000000..def9e69b77
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-terminal.js
@@ -0,0 +1,114 @@
+ace.define("ace/theme/terminal",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-terminal-theme";
+exports.cssText = ".ace-terminal-theme .ace_gutter {\
+background: #1a0005;\
+color: steelblue\
+}\
+.ace-terminal-theme .ace_print-margin {\
+width: 1px;\
+background: #1a1a1a\
+}\
+.ace-terminal-theme {\
+background-color: black;\
+color: #DEDEDE\
+}\
+.ace-terminal-theme .ace_cursor {\
+color: #9F9F9F\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_selection {\
+background: #424242\
+}\
+.ace-terminal-theme.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px black;\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_step {\
+background: rgb(0, 0, 0)\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_bracket {\
+background: #090;\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_bracket-start {\
+background: #090;\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_bracket-unmatched {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #900\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_active-line {\
+background: #2A2A2A\
+}\
+.ace-terminal-theme .ace_gutter-active-line {\
+background-color: #2A112A\
+}\
+.ace-terminal-theme .ace_marker-layer .ace_selected-word {\
+border: 1px solid #424242\
+}\
+.ace-terminal-theme .ace_invisible {\
+color: #343434\
+}\
+.ace-terminal-theme .ace_keyword,\
+.ace-terminal-theme .ace_meta,\
+.ace-terminal-theme .ace_storage,\
+.ace-terminal-theme .ace_storage.ace_type,\
+.ace-terminal-theme .ace_support.ace_type {\
+color: tomato\
+}\
+.ace-terminal-theme .ace_keyword.ace_operator {\
+color: deeppink\
+}\
+.ace-terminal-theme .ace_constant.ace_character,\
+.ace-terminal-theme .ace_constant.ace_language,\
+.ace-terminal-theme .ace_constant.ace_numeric,\
+.ace-terminal-theme .ace_keyword.ace_other.ace_unit,\
+.ace-terminal-theme .ace_support.ace_constant,\
+.ace-terminal-theme .ace_variable.ace_parameter {\
+color: #E78C45\
+}\
+.ace-terminal-theme .ace_constant.ace_other {\
+color: gold\
+}\
+.ace-terminal-theme .ace_invalid {\
+color: yellow;\
+background-color: red\
+}\
+.ace-terminal-theme .ace_invalid.ace_deprecated {\
+color: #CED2CF;\
+background-color: #B798BF\
+}\
+.ace-terminal-theme .ace_fold {\
+background-color: #7AA6DA;\
+border-color: #DEDEDE\
+}\
+.ace-terminal-theme .ace_entity.ace_name.ace_function,\
+.ace-terminal-theme .ace_support.ace_function,\
+.ace-terminal-theme .ace_variable {\
+color: #7AA6DA\
+}\
+.ace-terminal-theme .ace_support.ace_class,\
+.ace-terminal-theme .ace_support.ace_type {\
+color: #E7C547\
+}\
+.ace-terminal-theme .ace_heading,\
+.ace-terminal-theme .ace_string {\
+color: #B9CA4A\
+}\
+.ace-terminal-theme .ace_entity.ace_name.ace_tag,\
+.ace-terminal-theme .ace_entity.ace_other.ace_attribute-name,\
+.ace-terminal-theme .ace_meta.ace_tag,\
+.ace-terminal-theme .ace_string.ace_regexp,\
+.ace-terminal-theme .ace_variable {\
+color: #D54E53\
+}\
+.ace-terminal-theme .ace_comment {\
+color: orangered\
+}\
+.ace-terminal-theme .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLBWV/8PAAK4AYnhiq+xAAAAAElFTkSuQmCC) right repeat-y;\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-textmate.js b/services/web/public/js/ace-1.2.5/theme-textmate.js
new file mode 100644
index 0000000000..0033edae2c
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-textmate.js
@@ -0,0 +1,129 @@
+ace.define("ace/theme/textmate",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+"use strict";
+
+exports.isDark = false;
+exports.cssClass = "ace-tm";
+exports.cssText = ".ace-tm .ace_gutter {\
+background: #f0f0f0;\
+color: #333;\
+}\
+.ace-tm .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8;\
+}\
+.ace-tm .ace_fold {\
+background-color: #6B72E6;\
+}\
+.ace-tm {\
+background-color: #FFFFFF;\
+color: black;\
+}\
+.ace-tm .ace_cursor {\
+color: black;\
+}\
+.ace-tm .ace_invisible {\
+color: rgb(191, 191, 191);\
+}\
+.ace-tm .ace_storage,\
+.ace-tm .ace_keyword {\
+color: blue;\
+}\
+.ace-tm .ace_constant {\
+color: rgb(197, 6, 11);\
+}\
+.ace-tm .ace_constant.ace_buildin {\
+color: rgb(88, 72, 246);\
+}\
+.ace-tm .ace_constant.ace_language {\
+color: rgb(88, 92, 246);\
+}\
+.ace-tm .ace_constant.ace_library {\
+color: rgb(6, 150, 14);\
+}\
+.ace-tm .ace_invalid {\
+background-color: rgba(255, 0, 0, 0.1);\
+color: red;\
+}\
+.ace-tm .ace_support.ace_function {\
+color: rgb(60, 76, 114);\
+}\
+.ace-tm .ace_support.ace_constant {\
+color: rgb(6, 150, 14);\
+}\
+.ace-tm .ace_support.ace_type,\
+.ace-tm .ace_support.ace_class {\
+color: rgb(109, 121, 222);\
+}\
+.ace-tm .ace_keyword.ace_operator {\
+color: rgb(104, 118, 135);\
+}\
+.ace-tm .ace_string {\
+color: rgb(3, 106, 7);\
+}\
+.ace-tm .ace_comment {\
+color: rgb(76, 136, 107);\
+}\
+.ace-tm .ace_comment.ace_doc {\
+color: rgb(0, 102, 255);\
+}\
+.ace-tm .ace_comment.ace_doc.ace_tag {\
+color: rgb(128, 159, 191);\
+}\
+.ace-tm .ace_constant.ace_numeric {\
+color: rgb(0, 0, 205);\
+}\
+.ace-tm .ace_variable {\
+color: rgb(49, 132, 149);\
+}\
+.ace-tm .ace_xml-pe {\
+color: rgb(104, 104, 91);\
+}\
+.ace-tm .ace_entity.ace_name.ace_function {\
+color: #0000A2;\
+}\
+.ace-tm .ace_heading {\
+color: rgb(12, 7, 255);\
+}\
+.ace-tm .ace_list {\
+color:rgb(185, 6, 144);\
+}\
+.ace-tm .ace_meta.ace_tag {\
+color:rgb(0, 22, 142);\
+}\
+.ace-tm .ace_string.ace_regex {\
+color: rgb(255, 0, 0)\
+}\
+.ace-tm .ace_marker-layer .ace_selection {\
+background: rgb(181, 213, 255);\
+}\
+.ace-tm.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px white;\
+}\
+.ace-tm .ace_marker-layer .ace_step {\
+background: rgb(252, 255, 0);\
+}\
+.ace-tm .ace_marker-layer .ace_stack {\
+background: rgb(164, 229, 101);\
+}\
+.ace-tm .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgb(192, 192, 192);\
+}\
+.ace-tm .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.07);\
+}\
+.ace-tm .ace_gutter-active-line {\
+background-color : #dcdcdc;\
+}\
+.ace-tm .ace_marker-layer .ace_selected-word {\
+background: rgb(250, 250, 255);\
+border: 1px solid rgb(200, 200, 250);\
+}\
+.ace-tm .ace_indent-guide {\
+background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
+}\
+";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-tomorrow.js b/services/web/public/js/ace-1.2.5/theme-tomorrow.js
new file mode 100644
index 0000000000..4661be1122
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-tomorrow.js
@@ -0,0 +1,108 @@
+ace.define("ace/theme/tomorrow",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-tomorrow";
+exports.cssText = ".ace-tomorrow .ace_gutter {\
+background: #f6f6f6;\
+color: #4D4D4C\
+}\
+.ace-tomorrow .ace_print-margin {\
+width: 1px;\
+background: #f6f6f6\
+}\
+.ace-tomorrow {\
+background-color: #FFFFFF;\
+color: #4D4D4C\
+}\
+.ace-tomorrow .ace_cursor {\
+color: #AEAFAD\
+}\
+.ace-tomorrow .ace_marker-layer .ace_selection {\
+background: #D6D6D6\
+}\
+.ace-tomorrow.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #FFFFFF;\
+}\
+.ace-tomorrow .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0)\
+}\
+.ace-tomorrow .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #D1D1D1\
+}\
+.ace-tomorrow .ace_marker-layer .ace_active-line {\
+background: #EFEFEF\
+}\
+.ace-tomorrow .ace_gutter-active-line {\
+background-color : #dcdcdc\
+}\
+.ace-tomorrow .ace_marker-layer .ace_selected-word {\
+border: 1px solid #D6D6D6\
+}\
+.ace-tomorrow .ace_invisible {\
+color: #D1D1D1\
+}\
+.ace-tomorrow .ace_keyword,\
+.ace-tomorrow .ace_meta,\
+.ace-tomorrow .ace_storage,\
+.ace-tomorrow .ace_storage.ace_type,\
+.ace-tomorrow .ace_support.ace_type {\
+color: #8959A8\
+}\
+.ace-tomorrow .ace_keyword.ace_operator {\
+color: #3E999F\
+}\
+.ace-tomorrow .ace_constant.ace_character,\
+.ace-tomorrow .ace_constant.ace_language,\
+.ace-tomorrow .ace_constant.ace_numeric,\
+.ace-tomorrow .ace_keyword.ace_other.ace_unit,\
+.ace-tomorrow .ace_support.ace_constant,\
+.ace-tomorrow .ace_variable.ace_parameter {\
+color: #F5871F\
+}\
+.ace-tomorrow .ace_constant.ace_other {\
+color: #666969\
+}\
+.ace-tomorrow .ace_invalid {\
+color: #FFFFFF;\
+background-color: #C82829\
+}\
+.ace-tomorrow .ace_invalid.ace_deprecated {\
+color: #FFFFFF;\
+background-color: #8959A8\
+}\
+.ace-tomorrow .ace_fold {\
+background-color: #4271AE;\
+border-color: #4D4D4C\
+}\
+.ace-tomorrow .ace_entity.ace_name.ace_function,\
+.ace-tomorrow .ace_support.ace_function,\
+.ace-tomorrow .ace_variable {\
+color: #4271AE\
+}\
+.ace-tomorrow .ace_support.ace_class,\
+.ace-tomorrow .ace_support.ace_type {\
+color: #C99E00\
+}\
+.ace-tomorrow .ace_heading,\
+.ace-tomorrow .ace_markup.ace_heading,\
+.ace-tomorrow .ace_string {\
+color: #718C00\
+}\
+.ace-tomorrow .ace_entity.ace_name.ace_tag,\
+.ace-tomorrow .ace_entity.ace_other.ace_attribute-name,\
+.ace-tomorrow .ace_meta.ace_tag,\
+.ace-tomorrow .ace_string.ace_regexp,\
+.ace-tomorrow .ace_variable {\
+color: #C82829\
+}\
+.ace-tomorrow .ace_comment {\
+color: #8E908C\
+}\
+.ace-tomorrow .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-tomorrow_night.js b/services/web/public/js/ace-1.2.5/theme-tomorrow_night.js
new file mode 100644
index 0000000000..53e1f39a41
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-tomorrow_night.js
@@ -0,0 +1,108 @@
+ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-tomorrow-night";
+exports.cssText = ".ace-tomorrow-night .ace_gutter {\
+background: #25282c;\
+color: #C5C8C6\
+}\
+.ace-tomorrow-night .ace_print-margin {\
+width: 1px;\
+background: #25282c\
+}\
+.ace-tomorrow-night {\
+background-color: #1D1F21;\
+color: #C5C8C6\
+}\
+.ace-tomorrow-night .ace_cursor {\
+color: #AEAFAD\
+}\
+.ace-tomorrow-night .ace_marker-layer .ace_selection {\
+background: #373B41\
+}\
+.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #1D1F21;\
+}\
+.ace-tomorrow-night .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-tomorrow-night .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #4B4E55\
+}\
+.ace-tomorrow-night .ace_marker-layer .ace_active-line {\
+background: #282A2E\
+}\
+.ace-tomorrow-night .ace_gutter-active-line {\
+background-color: #282A2E\
+}\
+.ace-tomorrow-night .ace_marker-layer .ace_selected-word {\
+border: 1px solid #373B41\
+}\
+.ace-tomorrow-night .ace_invisible {\
+color: #4B4E55\
+}\
+.ace-tomorrow-night .ace_keyword,\
+.ace-tomorrow-night .ace_meta,\
+.ace-tomorrow-night .ace_storage,\
+.ace-tomorrow-night .ace_storage.ace_type,\
+.ace-tomorrow-night .ace_support.ace_type {\
+color: #B294BB\
+}\
+.ace-tomorrow-night .ace_keyword.ace_operator {\
+color: #8ABEB7\
+}\
+.ace-tomorrow-night .ace_constant.ace_character,\
+.ace-tomorrow-night .ace_constant.ace_language,\
+.ace-tomorrow-night .ace_constant.ace_numeric,\
+.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,\
+.ace-tomorrow-night .ace_support.ace_constant,\
+.ace-tomorrow-night .ace_variable.ace_parameter {\
+color: #DE935F\
+}\
+.ace-tomorrow-night .ace_constant.ace_other {\
+color: #CED1CF\
+}\
+.ace-tomorrow-night .ace_invalid {\
+color: #CED2CF;\
+background-color: #DF5F5F\
+}\
+.ace-tomorrow-night .ace_invalid.ace_deprecated {\
+color: #CED2CF;\
+background-color: #B798BF\
+}\
+.ace-tomorrow-night .ace_fold {\
+background-color: #81A2BE;\
+border-color: #C5C8C6\
+}\
+.ace-tomorrow-night .ace_entity.ace_name.ace_function,\
+.ace-tomorrow-night .ace_support.ace_function,\
+.ace-tomorrow-night .ace_variable {\
+color: #81A2BE\
+}\
+.ace-tomorrow-night .ace_support.ace_class,\
+.ace-tomorrow-night .ace_support.ace_type {\
+color: #F0C674\
+}\
+.ace-tomorrow-night .ace_heading,\
+.ace-tomorrow-night .ace_markup.ace_heading,\
+.ace-tomorrow-night .ace_string {\
+color: #B5BD68\
+}\
+.ace-tomorrow-night .ace_entity.ace_name.ace_tag,\
+.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,\
+.ace-tomorrow-night .ace_meta.ace_tag,\
+.ace-tomorrow-night .ace_string.ace_regexp,\
+.ace-tomorrow-night .ace_variable {\
+color: #CC6666\
+}\
+.ace-tomorrow-night .ace_comment {\
+color: #969896\
+}\
+.ace-tomorrow-night .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-tomorrow_night_blue.js b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_blue.js
new file mode 100644
index 0000000000..956e221ec9
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_blue.js
@@ -0,0 +1,106 @@
+ace.define("ace/theme/tomorrow_night_blue",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-tomorrow-night-blue";
+exports.cssText = ".ace-tomorrow-night-blue .ace_gutter {\
+background: #00204b;\
+color: #7388b5\
+}\
+.ace-tomorrow-night-blue .ace_print-margin {\
+width: 1px;\
+background: #00204b\
+}\
+.ace-tomorrow-night-blue {\
+background-color: #002451;\
+color: #FFFFFF\
+}\
+.ace-tomorrow-night-blue .ace_constant.ace_other,\
+.ace-tomorrow-night-blue .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-tomorrow-night-blue .ace_marker-layer .ace_selection {\
+background: #003F8E\
+}\
+.ace-tomorrow-night-blue.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #002451;\
+}\
+.ace-tomorrow-night-blue .ace_marker-layer .ace_step {\
+background: rgb(127, 111, 19)\
+}\
+.ace-tomorrow-night-blue .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #404F7D\
+}\
+.ace-tomorrow-night-blue .ace_marker-layer .ace_active-line {\
+background: #00346E\
+}\
+.ace-tomorrow-night-blue .ace_gutter-active-line {\
+background-color: #022040\
+}\
+.ace-tomorrow-night-blue .ace_marker-layer .ace_selected-word {\
+border: 1px solid #003F8E\
+}\
+.ace-tomorrow-night-blue .ace_invisible {\
+color: #404F7D\
+}\
+.ace-tomorrow-night-blue .ace_keyword,\
+.ace-tomorrow-night-blue .ace_meta,\
+.ace-tomorrow-night-blue .ace_storage,\
+.ace-tomorrow-night-blue .ace_storage.ace_type,\
+.ace-tomorrow-night-blue .ace_support.ace_type {\
+color: #EBBBFF\
+}\
+.ace-tomorrow-night-blue .ace_keyword.ace_operator {\
+color: #99FFFF\
+}\
+.ace-tomorrow-night-blue .ace_constant.ace_character,\
+.ace-tomorrow-night-blue .ace_constant.ace_language,\
+.ace-tomorrow-night-blue .ace_constant.ace_numeric,\
+.ace-tomorrow-night-blue .ace_keyword.ace_other.ace_unit,\
+.ace-tomorrow-night-blue .ace_support.ace_constant,\
+.ace-tomorrow-night-blue .ace_variable.ace_parameter {\
+color: #FFC58F\
+}\
+.ace-tomorrow-night-blue .ace_invalid {\
+color: #FFFFFF;\
+background-color: #F99DA5\
+}\
+.ace-tomorrow-night-blue .ace_invalid.ace_deprecated {\
+color: #FFFFFF;\
+background-color: #EBBBFF\
+}\
+.ace-tomorrow-night-blue .ace_fold {\
+background-color: #BBDAFF;\
+border-color: #FFFFFF\
+}\
+.ace-tomorrow-night-blue .ace_entity.ace_name.ace_function,\
+.ace-tomorrow-night-blue .ace_support.ace_function,\
+.ace-tomorrow-night-blue .ace_variable {\
+color: #BBDAFF\
+}\
+.ace-tomorrow-night-blue .ace_support.ace_class,\
+.ace-tomorrow-night-blue .ace_support.ace_type {\
+color: #FFEEAD\
+}\
+.ace-tomorrow-night-blue .ace_heading,\
+.ace-tomorrow-night-blue .ace_markup.ace_heading,\
+.ace-tomorrow-night-blue .ace_string {\
+color: #D1F1A9\
+}\
+.ace-tomorrow-night-blue .ace_entity.ace_name.ace_tag,\
+.ace-tomorrow-night-blue .ace_entity.ace_other.ace_attribute-name,\
+.ace-tomorrow-night-blue .ace_meta.ace_tag,\
+.ace-tomorrow-night-blue .ace_string.ace_regexp,\
+.ace-tomorrow-night-blue .ace_variable {\
+color: #FF9DA4\
+}\
+.ace-tomorrow-night-blue .ace_comment {\
+color: #7285B7\
+}\
+.ace-tomorrow-night-blue .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYJDzqfwPAANXAeNsiA+ZAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-tomorrow_night_bright.js b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_bright.js
new file mode 100644
index 0000000000..8514a0d699
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_bright.js
@@ -0,0 +1,121 @@
+ace.define("ace/theme/tomorrow_night_bright",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-tomorrow-night-bright";
+exports.cssText = ".ace-tomorrow-night-bright .ace_gutter {\
+background: #1a1a1a;\
+color: #DEDEDE\
+}\
+.ace-tomorrow-night-bright .ace_print-margin {\
+width: 1px;\
+background: #1a1a1a\
+}\
+.ace-tomorrow-night-bright {\
+background-color: #000000;\
+color: #DEDEDE\
+}\
+.ace-tomorrow-night-bright .ace_cursor {\
+color: #9F9F9F\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_selection {\
+background: #424242\
+}\
+.ace-tomorrow-night-bright.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #000000;\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #888888\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_highlight {\
+border: 1px solid rgb(110, 119, 0);\
+border-bottom: 0;\
+box-shadow: inset 0 -1px rgb(110, 119, 0);\
+margin: -1px 0 0 -1px;\
+background: rgba(255, 235, 0, 0.1)\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_active-line {\
+background: #2A2A2A\
+}\
+.ace-tomorrow-night-bright .ace_gutter-active-line {\
+background-color: #2A2A2A\
+}\
+.ace-tomorrow-night-bright .ace_stack {\
+background-color: rgb(66, 90, 44)\
+}\
+.ace-tomorrow-night-bright .ace_marker-layer .ace_selected-word {\
+border: 1px solid #888888\
+}\
+.ace-tomorrow-night-bright .ace_invisible {\
+color: #343434\
+}\
+.ace-tomorrow-night-bright .ace_keyword,\
+.ace-tomorrow-night-bright .ace_meta,\
+.ace-tomorrow-night-bright .ace_storage,\
+.ace-tomorrow-night-bright .ace_storage.ace_type,\
+.ace-tomorrow-night-bright .ace_support.ace_type {\
+color: #C397D8\
+}\
+.ace-tomorrow-night-bright .ace_keyword.ace_operator {\
+color: #70C0B1\
+}\
+.ace-tomorrow-night-bright .ace_constant.ace_character,\
+.ace-tomorrow-night-bright .ace_constant.ace_language,\
+.ace-tomorrow-night-bright .ace_constant.ace_numeric,\
+.ace-tomorrow-night-bright .ace_keyword.ace_other.ace_unit,\
+.ace-tomorrow-night-bright .ace_support.ace_constant,\
+.ace-tomorrow-night-bright .ace_variable.ace_parameter {\
+color: #E78C45\
+}\
+.ace-tomorrow-night-bright .ace_constant.ace_other {\
+color: #EEEEEE\
+}\
+.ace-tomorrow-night-bright .ace_invalid {\
+color: #CED2CF;\
+background-color: #DF5F5F\
+}\
+.ace-tomorrow-night-bright .ace_invalid.ace_deprecated {\
+color: #CED2CF;\
+background-color: #B798BF\
+}\
+.ace-tomorrow-night-bright .ace_fold {\
+background-color: #7AA6DA;\
+border-color: #DEDEDE\
+}\
+.ace-tomorrow-night-bright .ace_entity.ace_name.ace_function,\
+.ace-tomorrow-night-bright .ace_support.ace_function,\
+.ace-tomorrow-night-bright .ace_variable {\
+color: #7AA6DA\
+}\
+.ace-tomorrow-night-bright .ace_support.ace_class,\
+.ace-tomorrow-night-bright .ace_support.ace_type {\
+color: #E7C547\
+}\
+.ace-tomorrow-night-bright .ace_heading,\
+.ace-tomorrow-night-bright .ace_markup.ace_heading,\
+.ace-tomorrow-night-bright .ace_string {\
+color: #B9CA4A\
+}\
+.ace-tomorrow-night-bright .ace_entity.ace_name.ace_tag,\
+.ace-tomorrow-night-bright .ace_entity.ace_other.ace_attribute-name,\
+.ace-tomorrow-night-bright .ace_meta.ace_tag,\
+.ace-tomorrow-night-bright .ace_string.ace_regexp,\
+.ace-tomorrow-night-bright .ace_variable {\
+color: #D54E53\
+}\
+.ace-tomorrow-night-bright .ace_comment {\
+color: #969896\
+}\
+.ace-tomorrow-night-bright .ace_c9searchresults.ace_keyword {\
+color: #C2C280\
+}\
+.ace-tomorrow-night-bright .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYFBXV/8PAAJoAXX4kT2EAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-tomorrow_night_eighties.js b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_eighties.js
new file mode 100644
index 0000000000..3665e3f7dc
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-tomorrow_night_eighties.js
@@ -0,0 +1,108 @@
+ace.define("ace/theme/tomorrow_night_eighties",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-tomorrow-night-eighties";
+exports.cssText = ".ace-tomorrow-night-eighties .ace_gutter {\
+background: #272727;\
+color: #CCC\
+}\
+.ace-tomorrow-night-eighties .ace_print-margin {\
+width: 1px;\
+background: #272727\
+}\
+.ace-tomorrow-night-eighties {\
+background-color: #2D2D2D;\
+color: #CCCCCC\
+}\
+.ace-tomorrow-night-eighties .ace_constant.ace_other,\
+.ace-tomorrow-night-eighties .ace_cursor {\
+color: #CCCCCC\
+}\
+.ace-tomorrow-night-eighties .ace_marker-layer .ace_selection {\
+background: #515151\
+}\
+.ace-tomorrow-night-eighties.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #2D2D2D;\
+}\
+.ace-tomorrow-night-eighties .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-tomorrow-night-eighties .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #6A6A6A\
+}\
+.ace-tomorrow-night-bright .ace_stack {\
+background: rgb(66, 90, 44)\
+}\
+.ace-tomorrow-night-eighties .ace_marker-layer .ace_active-line {\
+background: #393939\
+}\
+.ace-tomorrow-night-eighties .ace_gutter-active-line {\
+background-color: #393939\
+}\
+.ace-tomorrow-night-eighties .ace_marker-layer .ace_selected-word {\
+border: 1px solid #515151\
+}\
+.ace-tomorrow-night-eighties .ace_invisible {\
+color: #6A6A6A\
+}\
+.ace-tomorrow-night-eighties .ace_keyword,\
+.ace-tomorrow-night-eighties .ace_meta,\
+.ace-tomorrow-night-eighties .ace_storage,\
+.ace-tomorrow-night-eighties .ace_storage.ace_type,\
+.ace-tomorrow-night-eighties .ace_support.ace_type {\
+color: #CC99CC\
+}\
+.ace-tomorrow-night-eighties .ace_keyword.ace_operator {\
+color: #66CCCC\
+}\
+.ace-tomorrow-night-eighties .ace_constant.ace_character,\
+.ace-tomorrow-night-eighties .ace_constant.ace_language,\
+.ace-tomorrow-night-eighties .ace_constant.ace_numeric,\
+.ace-tomorrow-night-eighties .ace_keyword.ace_other.ace_unit,\
+.ace-tomorrow-night-eighties .ace_support.ace_constant,\
+.ace-tomorrow-night-eighties .ace_variable.ace_parameter {\
+color: #F99157\
+}\
+.ace-tomorrow-night-eighties .ace_invalid {\
+color: #CDCDCD;\
+background-color: #F2777A\
+}\
+.ace-tomorrow-night-eighties .ace_invalid.ace_deprecated {\
+color: #CDCDCD;\
+background-color: #CC99CC\
+}\
+.ace-tomorrow-night-eighties .ace_fold {\
+background-color: #6699CC;\
+border-color: #CCCCCC\
+}\
+.ace-tomorrow-night-eighties .ace_entity.ace_name.ace_function,\
+.ace-tomorrow-night-eighties .ace_support.ace_function,\
+.ace-tomorrow-night-eighties .ace_variable {\
+color: #6699CC\
+}\
+.ace-tomorrow-night-eighties .ace_support.ace_class,\
+.ace-tomorrow-night-eighties .ace_support.ace_type {\
+color: #FFCC66\
+}\
+.ace-tomorrow-night-eighties .ace_heading,\
+.ace-tomorrow-night-eighties .ace_markup.ace_heading,\
+.ace-tomorrow-night-eighties .ace_string {\
+color: #99CC99\
+}\
+.ace-tomorrow-night-eighties .ace_comment {\
+color: #999999\
+}\
+.ace-tomorrow-night-eighties .ace_entity.ace_name.ace_tag,\
+.ace-tomorrow-night-eighties .ace_entity.ace_other.ace_attribute-name,\
+.ace-tomorrow-night-eighties .ace_meta.ace_tag,\
+.ace-tomorrow-night-eighties .ace_variable {\
+color: #F2777A\
+}\
+.ace-tomorrow-night-eighties .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ09NrYAgMjP4PAAtGAwchHMyAAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-twilight.js b/services/web/public/js/ace-1.2.5/theme-twilight.js
new file mode 100644
index 0000000000..48ec030981
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-twilight.js
@@ -0,0 +1,109 @@
+ace.define("ace/theme/twilight",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-twilight";
+exports.cssText = ".ace-twilight .ace_gutter {\
+background: #232323;\
+color: #E2E2E2\
+}\
+.ace-twilight .ace_print-margin {\
+width: 1px;\
+background: #232323\
+}\
+.ace-twilight {\
+background-color: #141414;\
+color: #F8F8F8\
+}\
+.ace-twilight .ace_cursor {\
+color: #A7A7A7\
+}\
+.ace-twilight .ace_marker-layer .ace_selection {\
+background: rgba(221, 240, 255, 0.20)\
+}\
+.ace-twilight.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #141414;\
+}\
+.ace-twilight .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-twilight .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid rgba(255, 255, 255, 0.25)\
+}\
+.ace-twilight .ace_marker-layer .ace_active-line {\
+background: rgba(255, 255, 255, 0.031)\
+}\
+.ace-twilight .ace_gutter-active-line {\
+background-color: rgba(255, 255, 255, 0.031)\
+}\
+.ace-twilight .ace_marker-layer .ace_selected-word {\
+border: 1px solid rgba(221, 240, 255, 0.20)\
+}\
+.ace-twilight .ace_invisible {\
+color: rgba(255, 255, 255, 0.25)\
+}\
+.ace-twilight .ace_keyword,\
+.ace-twilight .ace_meta {\
+color: #CDA869\
+}\
+.ace-twilight .ace_constant,\
+.ace-twilight .ace_constant.ace_character,\
+.ace-twilight .ace_constant.ace_character.ace_escape,\
+.ace-twilight .ace_constant.ace_other,\
+.ace-twilight .ace_heading,\
+.ace-twilight .ace_markup.ace_heading,\
+.ace-twilight .ace_support.ace_constant {\
+color: #CF6A4C\
+}\
+.ace-twilight .ace_invalid.ace_illegal {\
+color: #F8F8F8;\
+background-color: rgba(86, 45, 86, 0.75)\
+}\
+.ace-twilight .ace_invalid.ace_deprecated {\
+text-decoration: underline;\
+font-style: italic;\
+color: #D2A8A1\
+}\
+.ace-twilight .ace_support {\
+color: #9B859D\
+}\
+.ace-twilight .ace_fold {\
+background-color: #AC885B;\
+border-color: #F8F8F8\
+}\
+.ace-twilight .ace_support.ace_function {\
+color: #DAD085\
+}\
+.ace-twilight .ace_list,\
+.ace-twilight .ace_markup.ace_list,\
+.ace-twilight .ace_storage {\
+color: #F9EE98\
+}\
+.ace-twilight .ace_entity.ace_name.ace_function,\
+.ace-twilight .ace_meta.ace_tag,\
+.ace-twilight .ace_variable {\
+color: #AC885B\
+}\
+.ace-twilight .ace_string {\
+color: #8F9D6A\
+}\
+.ace-twilight .ace_string.ace_regexp {\
+color: #E9C062\
+}\
+.ace-twilight .ace_comment {\
+font-style: italic;\
+color: #5F5A60\
+}\
+.ace-twilight .ace_variable {\
+color: #7587A6\
+}\
+.ace-twilight .ace_xml-pe {\
+color: #494949\
+}\
+.ace-twilight .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMQERFpYLC1tf0PAAgOAnPnhxyiAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-vibrant_ink.js b/services/web/public/js/ace-1.2.5/theme-vibrant_ink.js
new file mode 100644
index 0000000000..db926c7052
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-vibrant_ink.js
@@ -0,0 +1,94 @@
+ace.define("ace/theme/vibrant_ink",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = true;
+exports.cssClass = "ace-vibrant-ink";
+exports.cssText = ".ace-vibrant-ink .ace_gutter {\
+background: #1a1a1a;\
+color: #BEBEBE\
+}\
+.ace-vibrant-ink .ace_print-margin {\
+width: 1px;\
+background: #1a1a1a\
+}\
+.ace-vibrant-ink {\
+background-color: #0F0F0F;\
+color: #FFFFFF\
+}\
+.ace-vibrant-ink .ace_cursor {\
+color: #FFFFFF\
+}\
+.ace-vibrant-ink .ace_marker-layer .ace_selection {\
+background: #6699CC\
+}\
+.ace-vibrant-ink.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #0F0F0F;\
+}\
+.ace-vibrant-ink .ace_marker-layer .ace_step {\
+background: rgb(102, 82, 0)\
+}\
+.ace-vibrant-ink .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #404040\
+}\
+.ace-vibrant-ink .ace_marker-layer .ace_active-line {\
+background: #333333\
+}\
+.ace-vibrant-ink .ace_gutter-active-line {\
+background-color: #333333\
+}\
+.ace-vibrant-ink .ace_marker-layer .ace_selected-word {\
+border: 1px solid #6699CC\
+}\
+.ace-vibrant-ink .ace_invisible {\
+color: #404040\
+}\
+.ace-vibrant-ink .ace_keyword,\
+.ace-vibrant-ink .ace_meta {\
+color: #FF6600\
+}\
+.ace-vibrant-ink .ace_constant,\
+.ace-vibrant-ink .ace_constant.ace_character,\
+.ace-vibrant-ink .ace_constant.ace_character.ace_escape,\
+.ace-vibrant-ink .ace_constant.ace_other {\
+color: #339999\
+}\
+.ace-vibrant-ink .ace_constant.ace_numeric {\
+color: #99CC99\
+}\
+.ace-vibrant-ink .ace_invalid,\
+.ace-vibrant-ink .ace_invalid.ace_deprecated {\
+color: #CCFF33;\
+background-color: #000000\
+}\
+.ace-vibrant-ink .ace_fold {\
+background-color: #FFCC00;\
+border-color: #FFFFFF\
+}\
+.ace-vibrant-ink .ace_entity.ace_name.ace_function,\
+.ace-vibrant-ink .ace_support.ace_function,\
+.ace-vibrant-ink .ace_variable {\
+color: #FFCC00\
+}\
+.ace-vibrant-ink .ace_variable.ace_parameter {\
+font-style: italic\
+}\
+.ace-vibrant-ink .ace_string {\
+color: #66FF00\
+}\
+.ace-vibrant-ink .ace_string.ace_regexp {\
+color: #44B4CC\
+}\
+.ace-vibrant-ink .ace_comment {\
+color: #9933CC\
+}\
+.ace-vibrant-ink .ace_entity.ace_other.ace_attribute-name {\
+font-style: italic;\
+color: #99CC99\
+}\
+.ace-vibrant-ink .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYNDTc/oPAALPAZ7hxlbYAAAAAElFTkSuQmCC) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/theme-xcode.js b/services/web/public/js/ace-1.2.5/theme-xcode.js
new file mode 100644
index 0000000000..3604a17029
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/theme-xcode.js
@@ -0,0 +1,88 @@
+ace.define("ace/theme/xcode",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-xcode";
+exports.cssText = "\
+.ace-xcode .ace_gutter {\
+background: #e8e8e8;\
+color: #333\
+}\
+.ace-xcode .ace_print-margin {\
+width: 1px;\
+background: #e8e8e8\
+}\
+.ace-xcode {\
+background-color: #FFFFFF;\
+color: #000000\
+}\
+.ace-xcode .ace_cursor {\
+color: #000000\
+}\
+.ace-xcode .ace_marker-layer .ace_selection {\
+background: #B5D5FF\
+}\
+.ace-xcode.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #FFFFFF;\
+}\
+.ace-xcode .ace_marker-layer .ace_step {\
+background: rgb(198, 219, 174)\
+}\
+.ace-xcode .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #BFBFBF\
+}\
+.ace-xcode .ace_marker-layer .ace_active-line {\
+background: rgba(0, 0, 0, 0.071)\
+}\
+.ace-xcode .ace_gutter-active-line {\
+background-color: rgba(0, 0, 0, 0.071)\
+}\
+.ace-xcode .ace_marker-layer .ace_selected-word {\
+border: 1px solid #B5D5FF\
+}\
+.ace-xcode .ace_constant.ace_language,\
+.ace-xcode .ace_keyword,\
+.ace-xcode .ace_meta,\
+.ace-xcode .ace_variable.ace_language {\
+color: #C800A4\
+}\
+.ace-xcode .ace_invisible {\
+color: #BFBFBF\
+}\
+.ace-xcode .ace_constant.ace_character,\
+.ace-xcode .ace_constant.ace_other {\
+color: #275A5E\
+}\
+.ace-xcode .ace_constant.ace_numeric {\
+color: #3A00DC\
+}\
+.ace-xcode .ace_entity.ace_other.ace_attribute-name,\
+.ace-xcode .ace_support.ace_constant,\
+.ace-xcode .ace_support.ace_function {\
+color: #450084\
+}\
+.ace-xcode .ace_fold {\
+background-color: #C800A4;\
+border-color: #000000\
+}\
+.ace-xcode .ace_entity.ace_name.ace_tag,\
+.ace-xcode .ace_support.ace_class,\
+.ace-xcode .ace_support.ace_type {\
+color: #790EAD\
+}\
+.ace-xcode .ace_storage {\
+color: #C900A4\
+}\
+.ace-xcode .ace_string {\
+color: #DF0002\
+}\
+.ace-xcode .ace_comment {\
+color: #008E00\
+}\
+.ace-xcode .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==) right repeat-y\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});
diff --git a/services/web/public/js/ace-1.2.5/worker-coffee.js b/services/web/public/js/ace-1.2.5/worker-coffee.js
new file mode 100644
index 0000000000..7fca260492
--- /dev/null
+++ b/services/web/public/js/ace-1.2.5/worker-coffee.js
@@ -0,0 +1,2157 @@
+"no use strict";
+;(function(window) {
+if (typeof window.window != "undefined" && window.document)
+ return;
+if (window.require && window.define)
+ return;
+
+if (!window.console) {
+ window.console = function() {
+ var msgs = Array.prototype.slice.call(arguments, 0);
+ postMessage({type: "log", data: msgs});
+ };
+ window.console.error =
+ window.console.warn =
+ window.console.log =
+ window.console.trace = window.console;
+}
+window.window = window;
+window.ace = window;
+
+window.onerror = function(message, file, line, col, err) {
+ postMessage({type: "error", data: {
+ message: message,
+ data: err.data,
+ file: file,
+ line: line,
+ col: col,
+ stack: err.stack
+ }});
+};
+
+window.normalizeModule = function(parentId, moduleName) {
+ // normalize plugin requires
+ if (moduleName.indexOf("!") !== -1) {
+ var chunks = moduleName.split("!");
+ return window.normalizeModule(parentId, chunks[0]) + "!" + window.normalizeModule(parentId, chunks[1]);
+ }
+ // normalize relative requires
+ if (moduleName.charAt(0) == ".") {
+ var base = parentId.split("/").slice(0, -1).join("/");
+ moduleName = (base ? base + "/" : "") + moduleName;
+
+ while (moduleName.indexOf(".") !== -1 && previous != moduleName) {
+ var previous = moduleName;
+ moduleName = moduleName.replace(/^\.\//, "").replace(/\/\.\//, "/").replace(/[^\/]+\/\.\.\//, "");
+ }
+ }
+
+ return moduleName;
+};
+
+window.require = function require(parentId, id) {
+ if (!id) {
+ id = parentId;
+ parentId = null;
+ }
+ if (!id.charAt)
+ throw new Error("worker.js require() accepts only (parentId, id) as arguments");
+
+ id = window.normalizeModule(parentId, id);
+
+ var module = window.require.modules[id];
+ if (module) {
+ if (!module.initialized) {
+ module.initialized = true;
+ module.exports = module.factory().exports;
+ }
+ return module.exports;
+ }
+
+ if (!window.require.tlns)
+ return console.log("unable to load " + id);
+
+ var path = resolveModuleId(id, window.require.tlns);
+ if (path.slice(-3) != ".js") path += ".js";
+
+ window.require.id = id;
+ window.require.modules[id] = {}; // prevent infinite loop on broken modules
+ importScripts(path);
+ return window.require(parentId, id);
+};
+function resolveModuleId(id, paths) {
+ var testPath = id, tail = "";
+ while (testPath) {
+ var alias = paths[testPath];
+ if (typeof alias == "string") {
+ return alias + tail;
+ } else if (alias) {
+ return alias.location.replace(/\/*$/, "/") + (tail || alias.main || alias.name);
+ } else if (alias === false) {
+ return "";
+ }
+ var i = testPath.lastIndexOf("/");
+ if (i === -1) break;
+ tail = testPath.substr(i) + tail;
+ testPath = testPath.slice(0, i);
+ }
+ return id;
+}
+window.require.modules = {};
+window.require.tlns = {};
+
+window.define = function(id, deps, factory) {
+ if (arguments.length == 2) {
+ factory = deps;
+ if (typeof id != "string") {
+ deps = id;
+ id = window.require.id;
+ }
+ } else if (arguments.length == 1) {
+ factory = id;
+ deps = [];
+ id = window.require.id;
+ }
+
+ if (typeof factory != "function") {
+ window.require.modules[id] = {
+ exports: factory,
+ initialized: true
+ };
+ return;
+ }
+
+ if (!deps.length)
+ // If there is no dependencies, we inject "require", "exports" and
+ // "module" as dependencies, to provide CommonJS compatibility.
+ deps = ["require", "exports", "module"];
+
+ var req = function(childId) {
+ return window.require(id, childId);
+ };
+
+ window.require.modules[id] = {
+ exports: {},
+ factory: function() {
+ var module = this;
+ var returnExports = factory.apply(this, deps.map(function(dep) {
+ switch (dep) {
+ // Because "require", "exports" and "module" aren't actual
+ // dependencies, we must handle them seperately.
+ case "require": return req;
+ case "exports": return module.exports;
+ case "module": return module;
+ // But for all other dependencies, we can just go ahead and
+ // require them.
+ default: return req(dep);
+ }
+ }));
+ if (returnExports)
+ module.exports = returnExports;
+ return module;
+ }
+ };
+};
+window.define.amd = {};
+require.tlns = {};
+window.initBaseUrls = function initBaseUrls(topLevelNamespaces) {
+ for (var i in topLevelNamespaces)
+ require.tlns[i] = topLevelNamespaces[i];
+};
+
+window.initSender = function initSender() {
+
+ var EventEmitter = window.require("ace/lib/event_emitter").EventEmitter;
+ var oop = window.require("ace/lib/oop");
+
+ var Sender = function() {};
+
+ (function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.callback = function(data, callbackId) {
+ postMessage({
+ type: "call",
+ id: callbackId,
+ data: data
+ });
+ };
+
+ this.emit = function(name, data) {
+ postMessage({
+ type: "event",
+ name: name,
+ data: data
+ });
+ };
+
+ }).call(Sender.prototype);
+
+ return new Sender();
+};
+
+var main = window.main = null;
+var sender = window.sender = null;
+
+window.onmessage = function(e) {
+ var msg = e.data;
+ if (msg.event && sender) {
+ sender._signal(msg.event, msg.data);
+ }
+ else if (msg.command) {
+ if (main[msg.command])
+ main[msg.command].apply(main, msg.args);
+ else if (window[msg.command])
+ window[msg.command].apply(window, msg.args);
+ else
+ throw new Error("Unknown command:" + msg.command);
+ }
+ else if (msg.init) {
+ window.initBaseUrls(msg.tlns);
+ require("ace/lib/es5-shim");
+ sender = window.sender = window.initSender();
+ var clazz = require(msg.module)[msg.classname];
+ main = window.main = new clazz(sender);
+ }
+};
+})(this);
+
+ace.define("ace/lib/oop",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+exports.inherits = function(ctor, superCtor) {
+ ctor.super_ = superCtor;
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+};
+
+exports.mixin = function(obj, mixin) {
+ for (var key in mixin) {
+ obj[key] = mixin[key];
+ }
+ return obj;
+};
+
+exports.implement = function(proto, mixin) {
+ exports.mixin(proto, mixin);
+};
+
+});
+
+ace.define("ace/range",["require","exports","module"], function(require, exports, module) {
+"use strict";
+var comparePoints = function(p1, p2) {
+ return p1.row - p2.row || p1.column - p2.column;
+};
+var Range = function(startRow, startColumn, endRow, endColumn) {
+ this.start = {
+ row: startRow,
+ column: startColumn
+ };
+
+ this.end = {
+ row: endRow,
+ column: endColumn
+ };
+};
+
+(function() {
+ this.isEqual = function(range) {
+ return this.start.row === range.start.row &&
+ this.end.row === range.end.row &&
+ this.start.column === range.start.column &&
+ this.end.column === range.end.column;
+ };
+ this.toString = function() {
+ return ("Range: [" + this.start.row + "/" + this.start.column +
+ "] -> [" + this.end.row + "/" + this.end.column + "]");
+ };
+
+ this.contains = function(row, column) {
+ return this.compare(row, column) == 0;
+ };
+ this.compareRange = function(range) {
+ var cmp,
+ end = range.end,
+ start = range.start;
+
+ cmp = this.compare(end.row, end.column);
+ if (cmp == 1) {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == 1) {
+ return 2;
+ } else if (cmp == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (cmp == -1) {
+ return -2;
+ } else {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == -1) {
+ return -1;
+ } else if (cmp == 1) {
+ return 42;
+ } else {
+ return 0;
+ }
+ }
+ };
+ this.comparePoint = function(p) {
+ return this.compare(p.row, p.column);
+ };
+ this.containsRange = function(range) {
+ return this.comparePoint(range.start) == 0 && this.comparePoint(range.end) == 0;
+ };
+ this.intersects = function(range) {
+ var cmp = this.compareRange(range);
+ return (cmp == -1 || cmp == 0 || cmp == 1);
+ };
+ this.isEnd = function(row, column) {
+ return this.end.row == row && this.end.column == column;
+ };
+ this.isStart = function(row, column) {
+ return this.start.row == row && this.start.column == column;
+ };
+ this.setStart = function(row, column) {
+ if (typeof row == "object") {
+ this.start.column = row.column;
+ this.start.row = row.row;
+ } else {
+ this.start.row = row;
+ this.start.column = column;
+ }
+ };
+ this.setEnd = function(row, column) {
+ if (typeof row == "object") {
+ this.end.column = row.column;
+ this.end.row = row.row;
+ } else {
+ this.end.row = row;
+ this.end.column = column;
+ }
+ };
+ this.inside = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column) || this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.insideStart = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.insideEnd = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ };
+ this.compare = function(row, column) {
+ if (!this.isMultiLine()) {
+ if (row === this.start.row) {
+ return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
+ }
+ }
+
+ if (row < this.start.row)
+ return -1;
+
+ if (row > this.end.row)
+ return 1;
+
+ if (this.start.row === row)
+ return column >= this.start.column ? 0 : -1;
+
+ if (this.end.row === row)
+ return column <= this.end.column ? 0 : 1;
+
+ return 0;
+ };
+ this.compareStart = function(row, column) {
+ if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.compareEnd = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.compareInside = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ };
+ this.clipRows = function(firstRow, lastRow) {
+ if (this.end.row > lastRow)
+ var end = {row: lastRow + 1, column: 0};
+ else if (this.end.row < firstRow)
+ var end = {row: firstRow, column: 0};
+
+ if (this.start.row > lastRow)
+ var start = {row: lastRow + 1, column: 0};
+ else if (this.start.row < firstRow)
+ var start = {row: firstRow, column: 0};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+ this.extend = function(row, column) {
+ var cmp = this.compare(row, column);
+
+ if (cmp == 0)
+ return this;
+ else if (cmp == -1)
+ var start = {row: row, column: column};
+ else
+ var end = {row: row, column: column};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+
+ this.isEmpty = function() {
+ return (this.start.row === this.end.row && this.start.column === this.end.column);
+ };
+ this.isMultiLine = function() {
+ return (this.start.row !== this.end.row);
+ };
+ this.clone = function() {
+ return Range.fromPoints(this.start, this.end);
+ };
+ this.collapseRows = function() {
+ if (this.end.column == 0)
+ return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0)
+ else
+ return new Range(this.start.row, 0, this.end.row, 0)
+ };
+ this.toScreenRange = function(session) {
+ var screenPosStart = session.documentToScreenPosition(this.start);
+ var screenPosEnd = session.documentToScreenPosition(this.end);
+
+ return new Range(
+ screenPosStart.row, screenPosStart.column,
+ screenPosEnd.row, screenPosEnd.column
+ );
+ };
+ this.moveBy = function(row, column) {
+ this.start.row += row;
+ this.start.column += column;
+ this.end.row += row;
+ this.end.column += column;
+ };
+
+}).call(Range.prototype);
+Range.fromPoints = function(start, end) {
+ return new Range(start.row, start.column, end.row, end.column);
+};
+Range.comparePoints = comparePoints;
+
+Range.comparePoints = function(p1, p2) {
+ return p1.row - p2.row || p1.column - p2.column;
+};
+
+
+exports.Range = Range;
+});
+
+ace.define("ace/apply_delta",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+function throwDeltaError(delta, errorText){
+ console.log("Invalid Delta:", delta);
+ throw "Invalid Delta: " + errorText;
+}
+
+function positionInDocument(docLines, position) {
+ return position.row >= 0 && position.row < docLines.length &&
+ position.column >= 0 && position.column <= docLines[position.row].length;
+}
+
+function validateDelta(docLines, delta) {
+ if (delta.action != "insert" && delta.action != "remove")
+ throwDeltaError(delta, "delta.action must be 'insert' or 'remove'");
+ if (!(delta.lines instanceof Array))
+ throwDeltaError(delta, "delta.lines must be an Array");
+ if (!delta.start || !delta.end)
+ throwDeltaError(delta, "delta.start/end must be an present");
+ var start = delta.start;
+ if (!positionInDocument(docLines, delta.start))
+ throwDeltaError(delta, "delta.start must be contained in document");
+ var end = delta.end;
+ if (delta.action == "remove" && !positionInDocument(docLines, end))
+ throwDeltaError(delta, "delta.end must contained in document for 'remove' actions");
+ var numRangeRows = end.row - start.row;
+ var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0));
+ if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars)
+ throwDeltaError(delta, "delta.range must match delta lines");
+}
+
+exports.applyDelta = function(docLines, delta, doNotValidate) {
+
+ var row = delta.start.row;
+ var startColumn = delta.start.column;
+ var line = docLines[row] || "";
+ switch (delta.action) {
+ case "insert":
+ var lines = delta.lines;
+ if (lines.length === 1) {
+ docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
+ } else {
+ var args = [row, 1].concat(delta.lines);
+ docLines.splice.apply(docLines, args);
+ docLines[row] = line.substring(0, startColumn) + docLines[row];
+ docLines[row + delta.lines.length - 1] += line.substring(startColumn);
+ }
+ break;
+ case "remove":
+ var endColumn = delta.end.column;
+ var endRow = delta.end.row;
+ if (row === endRow) {
+ docLines[row] = line.substring(0, startColumn) + line.substring(endColumn);
+ } else {
+ docLines.splice(
+ row, endRow - row + 1,
+ line.substring(0, startColumn) + docLines[endRow].substring(endColumn)
+ );
+ }
+ break;
+ }
+}
+});
+
+ace.define("ace/lib/event_emitter",["require","exports","module"], function(require, exports, module) {
+"use strict";
+
+var EventEmitter = {};
+var stopPropagation = function() { this.propagationStopped = true; };
+var preventDefault = function() { this.defaultPrevented = true; };
+
+EventEmitter._emit =
+EventEmitter._dispatchEvent = function(eventName, e) {
+ this._eventRegistry || (this._eventRegistry = {});
+ this._defaultHandlers || (this._defaultHandlers = {});
+
+ var listeners = this._eventRegistry[eventName] || [];
+ var defaultHandler = this._defaultHandlers[eventName];
+ if (!listeners.length && !defaultHandler)
+ return;
+
+ if (typeof e != "object" || !e)
+ e = {};
+
+ if (!e.type)
+ e.type = eventName;
+ if (!e.stopPropagation)
+ e.stopPropagation = stopPropagation;
+ if (!e.preventDefault)
+ e.preventDefault = preventDefault;
+
+ listeners = listeners.slice();
+ for (var i=0; i