diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 428e4d506d..0e3329c41f 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -7,18 +7,69 @@ module.exports = (grunt) -> grunt.loadNpmTasks 'grunt-mocha-test' grunt.loadNpmTasks 'grunt-available-tasks' grunt.loadNpmTasks 'grunt-contrib-requirejs' - grunt.loadNpmTasks 'grunt-execute' grunt.loadNpmTasks 'grunt-bunyan' grunt.loadNpmTasks 'grunt-sed' grunt.loadNpmTasks 'grunt-git-rev-parse' grunt.loadNpmTasks 'grunt-file-append' grunt.loadNpmTasks 'grunt-file-append' grunt.loadNpmTasks 'grunt-env' + grunt.loadNpmTasks 'grunt-newer' + grunt.loadNpmTasks 'grunt-contrib-watch' + grunt.loadNpmTasks 'grunt-parallel' + grunt.loadNpmTasks 'grunt-exec' + # grunt.loadNpmTasks 'grunt-contrib-imagemin' + # grunt.loadNpmTasks 'grunt-sprity' config = - execute: - app: - src: "app.js" + + exec: + run: + command:"node app.js | ./node_modules/logger-sharelatex/node_modules/bunyan/bin/bunyan --color" + cssmin: + command:"node_modules/clean-css/bin/cleancss --s0 -o public/stylesheets/style.css public/stylesheets/style.css" + + + watch: + coffee: + files: 'public/**/*.coffee' + tasks: ['quickcompile:coffee'] + options: {} + + less: + files: '**/*.less' + tasks: ['compile:css'] + options: {} + + + parallel: + run: + tasks:['exec', 'watch'] + options: + grunt:true + stream:true + + + # imagemin: + # dynamic: + # files: [{ + # expand: true + # cwd: 'public/img/' + # src: ['**/*.{png,jpg,gif}'] + # dest: 'public/img/' + # }] + # options: + # interlaced:false + # optimizationLevel: 7 + + # sprity: + # sprite: + # options: + # cssPath:"/img/" + # 'style': '../../public/stylesheets/app/sprites.less' + # margin: 0 + # src: ['./public/img/flags/24/*.png'] + # dest: './public/img/sprite' + coffee: app_dir: @@ -84,11 +135,16 @@ module.exports = (grunt) -> files: "public/stylesheets/style.css": "public/stylesheets/style.less" + + + env: run: add: NODE_TLS_REJECT_UNAUTHORIZED:0 + + requirejs: compile: options: @@ -170,44 +226,47 @@ module.exports = (grunt) -> pattern: "@@RELEASE@@" replacement: process.env.BUILD_NUMBER || "(unknown build)" + + + availabletasks: tasks: options: - filter: 'exclude', - tasks: [ - 'coffee' - 'less' - 'clean' - 'mochaTest' - 'availabletasks' - 'wrap_sharejs' - 'requirejs' - 'execute' - 'bunyan' - ] - groups: - "Compile tasks": [ - "compile:server" - "compile:client" - "compile:tests" - "compile" - "compile:unit_tests" - "compile:smoke_tests" - "compile:css" - "compile:minify" - "install" - ] - "Test tasks": [ - "test:unit" - "test:acceptance" - ] - "Run tasks": [ - "run" - "default" - ] - "Misc": [ - "help" - ] + filter: 'exclude', + tasks: [ + 'coffee' + 'less' + 'clean' + 'mochaTest' + 'availabletasks' + 'wrap_sharejs' + 'requirejs' + 'execute' + 'bunyan' + ] + groups: + "Compile tasks": [ + "compile:server" + "compile:client" + "compile:tests" + "compile" + "compile:unit_tests" + "compile:smoke_tests" + "compile:css" + "compile:minify" + "install" + ] + "Test tasks": [ + "test:unit" + "test:acceptance" + ] + "Run tasks": [ + "run" + "default" + ] + "Misc": [ + "help" + ] moduleCompileServerTasks = [] moduleCompileUnitTestTasks = [] @@ -304,12 +363,14 @@ module.exports = (grunt) -> grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server'] grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes'] grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] - grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append"] + grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests'] grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests'] grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests'] grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css'] + grunt.registerTask 'quickcompile:coffee', 'Compiles only changed coffee files',['newer:coffee'] + grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile'] @@ -319,11 +380,13 @@ 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', 'bunyan', 'env:run', 'execute'] + grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel'] + 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") @@ -356,4 +419,24 @@ module.exports = (grunt) -> #{settings.siteUrl}/user/password/set?passwordResetToken=#{token} """ - done() \ No newline at end of file + 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/README.md b/services/web/README.md index 00d94343ab..f777e7e5f5 100644 --- a/services/web/README.md +++ b/services/web/README.md @@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains a lot of logic around creating and editing projects, and account management. + The rest of the ShareLaTeX stack, along with information about contributing can be found in the [sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository. +Build process +---------------- + +web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets. + +Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`). + Unit test status ---------------- @@ -42,3 +50,11 @@ in the `public/img/iconshock` directory found via [findicons.com](http://findicons.com/icon/498089/height?id=526085#) +## Acceptance Tests + +To run the Acceptance tests: + +- set `allowPublicAccess` to true, either in the configuration file, + or by setting the environment variable `SHARELATEX_ALLOW_PUBLIC_ACCESS` to `true` +- start the server (`grunt`) +- in a separate terminal, run `grunt test:acceptance` diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e7db5d9f65..d75bef5207 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -9,11 +9,12 @@ Url = require("url") Settings = require "settings-sharelatex" basicAuth = require('basic-auth-connect') UserHandler = require("../User/UserHandler") +UserSessionsManager = require("../User/UserSessionsManager") 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 @@ -61,7 +62,7 @@ module.exports = AuthenticationController = requireLogin: () -> doRequest = (req, res, next = (error) ->) -> if !req.session.user? - AuthenticationController._redirectToLoginOrRegisterPage(req, res) + AuthenticationController._redirectToLoginOrRegisterPage(req, res) else req.user = req.session.user return next() @@ -92,9 +93,9 @@ module.exports = AuthenticationController = _redirectToLoginOrRegisterPage: (req, res)-> if req.query.zipUrl? or req.query.project_name? - return AuthenticationController._redirectToRegisterPage(req, res) + return AuthenticationController._redirectToRegisterPage(req, res) else - AuthenticationController._redirectToLoginPage(req, res) + AuthenticationController._redirectToLoginPage(req, res) _redirectToLoginPage: (req, res) -> @@ -132,6 +133,8 @@ module.exports = AuthenticationController = 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 @@ -141,4 +144,6 @@ module.exports = AuthenticationController = 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 201643e88e..bfcd55855d 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee @@ -1,8 +1,11 @@ +Settings = require "settings-sharelatex" User = require("../../models/User").User {db, ObjectId} = require("../../infrastructure/mongojs") crypto = require 'crypto' bcrypt = require 'bcrypt' +BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12 + module.exports = AuthenticationManager = authenticate: (query, password, callback = (error, user) ->) -> # Using Mongoose for legacy reasons here. The returned User instance @@ -15,7 +18,9 @@ module.exports = AuthenticationManager = bcrypt.compare password, user.hashedPassword, (error, match) -> return callback(error) if error? if match - callback null, user + AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) -> + return callback(err) if err? + callback null, user else callback null, null else @@ -24,7 +29,7 @@ module.exports = AuthenticationManager = callback null, null setUserPassword: (user_id, password, callback = (error) ->) -> - bcrypt.genSalt 7, (error, salt) -> + bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) -> return callback(error) if error? bcrypt.hash password, salt, (error, hash) -> return callback(error) if error? @@ -35,3 +40,10 @@ module.exports = AuthenticationManager = $unset: password: true }, callback) + checkRounds: (user, hashedPassword, password, callback = (error) ->) -> + # check current number of rounds and rehash if necessary + currentRounds = bcrypt.getRounds hashedPassword + if currentRounds < BCRYPT_ROUNDS + AuthenticationManager.setUserPassword user._id, password, callback + else + callback() diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee new file mode 100644 index 0000000000..a986eb8e4c --- /dev/null +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee @@ -0,0 +1,39 @@ +BetaProgramHandler = require './BetaProgramHandler' +UserLocator = require "../User/UserLocator" +Settings = require "settings-sharelatex" +logger = require 'logger-sharelatex' + + +module.exports = BetaProgramController = + + optIn: (req, res, next) -> + user_id = req?.session?.user?._id + logger.log {user_id}, "user opting in to beta program" + if !user_id + return next(new Error("no user id in session")) + BetaProgramHandler.optIn user_id, (err) -> + if err + return next(err) + return res.redirect "/beta/participate" + + optOut: (req, res, next) -> + user_id = req?.session?.user?._id + logger.log {user_id}, "user opting out of beta program" + if !user_id + return next(new Error("no user id in session")) + BetaProgramHandler.optOut user_id, (err) -> + if err + return next(err) + return res.redirect "/beta/participate" + + optInPage: (req, res, next)-> + user_id = req.session?.user?._id + logger.log {user_id}, "showing beta participation page for user" + UserLocator.findById user_id, (err, user)-> + if err + logger.err {err, user_id}, "error fetching user" + return next(err) + res.render 'beta_program/opt_in', + title:'sharelatex_beta_program' + user: user, + languages: Settings.languages, diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee new file mode 100644 index 0000000000..788782d578 --- /dev/null +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramHandler.coffee @@ -0,0 +1,31 @@ +User = require("../../models/User").User +logger = require 'logger-sharelatex' +metrics = require("../../infrastructure/Metrics") + +module.exports = BetaProgramHandler = + + optIn: (user_id, callback=(err)->) -> + User.findById user_id, (err, user) -> + if err + logger.err {err, user_id}, "problem adding user to beta" + return callback(err) + metrics.inc "beta-program.opt-in" + user.betaProgram = true + user.save (err) -> + if err + logger.err {err, user_id}, "problem adding user to beta" + return callback(err) + return callback(null) + + optOut: (user_id, callback=(err)->) -> + User.findById user_id, (err, user) -> + if err + logger.err {err, user_id}, "problem removing user from beta" + return callback(err) + metrics.inc "beta-program.opt-out" + user.betaProgram = false + user.save (err) -> + if err + logger.err {err, user_id}, "problem removing user from beta" + return callback(err) + return callback(null) diff --git a/services/web/app/coffee/Features/Blog/BlogController.coffee b/services/web/app/coffee/Features/Blog/BlogController.coffee index 8f726602c3..4aaee6962a 100644 --- a/services/web/app/coffee/Features/Blog/BlogController.coffee +++ b/services/web/app/coffee/Features/Blog/BlogController.coffee @@ -20,13 +20,15 @@ module.exports = BlogController = logger.log url:url, "proxying request to blog api" request.get blogUrl, (err, r, data)-> - if r?.statusCode == 404 + if r?.statusCode == 404 or r?.statusCode == 403 return ErrorController.notFound(req, res, next) if err? return res.send 500 data = data.trim() try data = JSON.parse(data) + if settings.cdn?.web?.host? + 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/Collaborators/CollaboratorsHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee index 71737eecff..81557fea42 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsHandler.coffee @@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler = getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) -> CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) -> return callback(error) if error? + result = [] async.mapLimit members, 3, (member, cb) -> UserGetter.getUser member.id, (error, user) -> return cb(error) if error? - return cb(null, { user: user, privilegeLevel: member.privilegeLevel }) - callback + if user? + result.push { user: user, privilegeLevel: member.privilegeLevel } + cb() + (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) @@ -82,6 +87,18 @@ module.exports = CollaboratorsHandler = 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? + allProjects = readAndWriteProjects.concat(readOnlyProjects) + jobs = [] + for project in allProjects + do (project) -> + jobs.push (cb) -> + 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() diff --git a/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee b/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee new file mode 100644 index 0000000000..b285d0e6b7 --- /dev/null +++ b/services/web/app/coffee/Features/Compile/ClsiFormatChecker.coffee @@ -0,0 +1,68 @@ +_ = require("lodash") +async = require("async") + +module.exports = ClsiFormatChecker = + + checkRecoursesForProblems: (resources, callback)-> + jobs = + conflictedPaths: (cb)-> + ClsiFormatChecker._checkForConflictingPaths resources, cb + + sizeCheck: (cb)-> + ClsiFormatChecker._checkDocsAreUnderSizeLimit resources, cb + + async.series jobs, (err, problems)-> + if err? + return callback(err) + + problems = _.omitBy(problems, _.isEmpty) + + if _.isEmpty(problems) + return callback() + else + callback(null, problems) + + + _checkForConflictingPaths: (resources, callback)-> + paths = _.map(resources, 'path') + + conflicts = _.filter paths, (path)-> + matchingPaths = _.filter paths, (checkPath)-> + return checkPath.indexOf(path+"/") != -1 + + return matchingPaths.length > 0 + + conflictObjects = _.map conflicts, (conflict)-> + path:conflict + + callback null, conflictObjects + + _checkDocsAreUnderSizeLimit: (resources, callback)-> + + FIVEMB = 1000 * 1000 * 5 + + totalSize = 0 + + sizedResources = _.map resources, (resource)-> + result = {path:resource.path} + if resource.content? + result.size = resource.content.replace(/\n/g).length + result.kbSize = Math.ceil(result.size / 1000) + else + result.size = 0 + totalSize += result.size + return result + + tooLarge = totalSize > FIVEMB + if !tooLarge + return callback() + else + sizedResources = _.sortBy(sizedResources, "size").reverse().slice(0, 10) + return callback(null, {resources:sizedResources, totalSize:totalSize}) + + + + + + + diff --git a/services/web/app/coffee/Features/Compile/ClsiManager.coffee b/services/web/app/coffee/Features/Compile/ClsiManager.coffee index 9af1602d2c..568d806e99 100755 --- a/services/web/app/coffee/Features/Compile/ClsiManager.coffee +++ b/services/web/app/coffee/Features/Compile/ClsiManager.coffee @@ -7,15 +7,24 @@ ProjectEntityHandler = require("../Project/ProjectEntityHandler") logger = require "logger-sharelatex" Url = require("url") ClsiCookieManager = require("./ClsiCookieManager") - +_ = require("underscore") +async = require("async") +ClsiFormatChecker = require("./ClsiFormatChecker") module.exports = ClsiManager = - sendRequest: (project_id, options = {}, callback = (error, success) ->) -> + sendRequest: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> return callback(error) if error? logger.log project_id: project_id, "sending compile to CLSI" - ClsiManager._postToClsi project_id, req, options.compileGroup, (error, response) -> + ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)-> + if err? + logger.err err, project_id, "could not check resources for potential problems before sending to clsi" + return callback(err) + if validationProblems? + logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted" + return callback(null, "validation-problems", null, null, validationProblems) + ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) -> if error? logger.err err:error, project_id:project_id, "error sending request to clsi" return callback(error) @@ -27,13 +36,19 @@ module.exports = ClsiManager = outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles) callback(null, response?.compile?.status, outputFiles, clsiServerId) - deleteAuxFiles: (project_id, options, callback = (error) ->) -> - compilerUrl = @_getCompilerUrl(options?.compileGroup) + stopCompile: (project_id, user_id, options, callback = (error) ->) -> + compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop") opts = - url:"#{compilerUrl}/project/#{project_id}" - method:"DELETE" + url:compilerUrl + method:"POST" ClsiManager._makeRequest project_id, opts, callback + deleteAuxFiles: (project_id, user_id, options, callback = (error) ->) -> + compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id) + opts = + url:compilerUrl + method:"DELETE" + ClsiManager._makeRequest project_id, opts, callback _makeRequest: (project_id, opts, callback)-> ClsiCookieManager.getCookieJar project_id, (err, jar)-> @@ -51,14 +66,17 @@ module.exports = ClsiManager = return callback err, response, body + _getCompilerUrl: (compileGroup, project_id, user_id, action) -> + host = Settings.apis.clsi.url + path = "/project/#{project_id}" + path += "/user/#{user_id}" if user_id? + path += "/#{action}" if action? + return "#{host}#{path}" - _getCompilerUrl: (compileGroup) -> - return Settings.apis.clsi.url - - _postToClsi: (project_id, req, compileGroup, callback = (error, response) ->) -> - compilerUrl = Settings.apis.clsi.url - opts = - url: "#{compilerUrl}/project/#{project_id}/compile" + _postToClsi: (project_id, user_id, req, compileGroup, callback = (error, response) ->) -> + compileUrl = @_getCompilerUrl(compileGroup, project_id, user_id, "compile") + opts = + url: compileUrl json: req method: "POST" ClsiManager._makeRequest project_id, opts, (error, response, body) -> @@ -75,10 +93,9 @@ module.exports = ClsiManager = _parseOutputFiles: (project_id, rawOutputFiles = []) -> outputFiles = [] for file in rawOutputFiles - path = Url.parse(file.url).path - path = path.replace("/project/#{project_id}/output/", "") outputFiles.push - path: path + path: file.path # the clsi is now sending this to web + url: Url.parse(file.url).path # the location of the file on the clsi, excluding the host part type: file.type build: file.build return outputFiles @@ -134,15 +151,15 @@ module.exports = ClsiManager = resources: resources } - wordCount: (project_id, file, options, callback = (error, response) ->) -> + wordCount: (project_id, user_id, file, options, callback = (error, response) ->) -> ClsiManager._buildRequest project_id, options, (error, req) -> - compilerUrl = ClsiManager._getCompilerUrl(options?.compileGroup) filename = file || req?.compile?.rootResourcePath - wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}" - if req.compile.options.imageName? - wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}" + wordcount_url = ClsiManager._getCompilerUrl(options?.compileGroup, project_id, user_id, "wordcount") opts = url: wordcount_url + qs: + file: filename + image: req.compile.options.imageName method: "GET" ClsiManager._makeRequest project_id, opts, (error, response, body) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Compile/CompileController.coffee b/services/web/app/coffee/Features/Compile/CompileController.coffee index b50c6ffd1a..f3680dc38e 100755 --- a/services/web/app/coffee/Features/Compile/CompileController.coffee +++ b/services/web/app/coffee/Features/Compile/CompileController.coffee @@ -29,8 +29,8 @@ module.exports = CompileController = options.compiler = req.body.compiler if req.body?.draft options.draft = req.body.draft - logger.log {options, project_id}, "got compile request" - CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits) -> + 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 { @@ -38,8 +38,32 @@ module.exports = CompileController = 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) -> + 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() + + _compileAsUser: (req, callback) -> + # callback with user_id if per-user, undefined otherwise + if not Settings.disablePerUserCompiles + AuthenticationController.getLoggedInUserId req, callback # -> (error, 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) + else + callback() # do a per-project compile, not per-user + downloadPdf: (req, res, next = (error) ->)-> Metrics.inc "pdf-downloads" project_id = req.params.Project_id @@ -72,16 +96,22 @@ module.exports = CompileController = logger.log project_id:project_id, ip:req.ip, "rate limit hit downloading pdf" res.send 500 else - CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/output.pdf", req, res, next) + CompileController._downloadAsUser req, (error, user_id) -> + url = CompileController._getFileUrl project_id, user_id, req.params.build_id, "output.pdf" + CompileController.proxyToClsi(project_id, url, req, res, next) deleteAuxFiles: (req, res, next) -> project_id = req.params.Project_id - CompileManager.deleteAuxFiles project_id, (error) -> + CompileController._compileAsUser req, (error, user_id) -> return next(error) if error? - res.sendStatus(200) + CompileManager.deleteAuxFiles project_id, user_id, (error) -> + return next(error) if error? + res.sendStatus(200) + # this is only used by templates, so is not called with a user_id compileAndDownloadPdf: (req, res, next)-> project_id = req.params.project_id + # pass user_id as null, since templates are an "anonymous" compile CompileManager.compile project_id, null, {}, (err)-> if err? logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf" @@ -91,12 +121,28 @@ module.exports = CompileController = getFileFromClsi: (req, res, next = (error) ->) -> project_id = req.params.Project_id - build = req.params.build - if build? - url = "/project/#{project_id}/build/#{build}/output/#{req.params.file}" + CompileController._downloadAsUser req, (error, user_id) -> + return next(error) if error? + url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file + CompileController.proxyToClsi(project_id, url, req, res, next) + + # compute a GET file url for a given project, user (optional), build (optional) and file + _getFileUrl: (project_id, user_id, build_id, file) -> + if user_id? and build_id? + url = "/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{file}" + else if user_id? + url = "/project/#{project_id}/user/#{user_id}/output/#{file}" + else if build_id? + url = "/project/#{project_id}/build/#{build_id}/output/#{file}" else - url = "/project/#{project_id}/output/#{req.params.file}" - CompileController.proxyToClsi(project_id, url, req, res, next) + url = "/project/#{project_id}/output/#{file}" + return url + + # compute a POST url for a project, user (optional) and action + _getUrl: (project_id, user_id, action) -> + path = "/project/#{project_id}" + path += "/user/#{user_id}" if user_id? + return "#{path}/#{action}" proxySyncPdf: (req, res, next = (error) ->) -> project_id = req.params.Project_id @@ -107,20 +153,35 @@ module.exports = CompileController = return next(new Error("invalid h parameter")) if not v?.match(/^\d+\.\d+$/) return next(new Error("invalid v parameter")) - destination = {url: "/project/#{project_id}/sync/pdf", qs: {page, h, v}} - CompileController.proxyToClsi(project_id, destination, req, res, next) + # whether this request is going to a per-user container + CompileController._compileAsUser req, (error, user_id) -> + return next(error) if error? + url = CompileController._getUrl(project_id, user_id, "sync/pdf") + destination = {url: url, qs: {page, h, v}} + CompileController.proxyToClsi(project_id, destination, req, res, next) proxySyncCode: (req, res, next = (error) ->) -> project_id = req.params.Project_id {file, line, column} = req.query - if not file? or Path.resolve("/", file) isnt "/#{file}" + if not file? + return next(new Error("missing file parameter")) + # Check that we are dealing with a simple file path (this is not + # strictly needed because synctex uses this parameter as a label + # to look up in the synctex output, and does not open the file + # itself). Since we have valid synctex paths like foo/./bar we + # allow those by replacing /./ with / + testPath = file.replace '/./', '/' + if Path.resolve("/", testPath) isnt "/#{testPath}" return next(new Error("invalid file parameter")) if not line?.match(/^\d+$/) return next(new Error("invalid line parameter")) if not column?.match(/^\d+$/) return next(new Error("invalid column parameter")) - destination = {url:"/project/#{project_id}/sync/code", qs: {file, line, column}} - CompileController.proxyToClsi(project_id, destination, req, res, next) + CompileController._compileAsUser req, (error, user_id) -> + return next(error) if error? + url = CompileController._getUrl(project_id, user_id, "sync/code") + destination = {url:url, qs: {file, line, column}} + CompileController.proxyToClsi(project_id, destination, req, res, next) proxyToClsi: (project_id, url, req, res, next = (error) ->) -> if req.query?.compileGroup @@ -138,10 +199,7 @@ module.exports = CompileController = # expand any url parameter passed in as {url:..., qs:...} if typeof url is "object" {url, qs} = url - if limits.compileGroup == "priority" - compilerUrl = Settings.apis.clsi_priority.url - else - compilerUrl = Settings.apis.clsi.url + compilerUrl = Settings.apis.clsi.url url = "#{compilerUrl}#{url}" logger.log url: url, "proxying to CLSI" oneMinute = 60 * 1000 @@ -168,7 +226,9 @@ module.exports = CompileController = wordCount: (req, res, next) -> project_id = req.params.Project_id file = req.query.file || false - CompileManager.wordCount project_id, file, (error, body) -> + CompileController._compileAsUser req, (error, user_id) -> return next(error) if error? - res.contentType("application/json") - res.send body + CompileManager.wordCount project_id, user_id, file, (error, body) -> + return next(error) if error? + res.contentType("application/json") + res.send body diff --git a/services/web/app/coffee/Features/Compile/CompileManager.coffee b/services/web/app/coffee/Features/Compile/CompileManager.coffee index 0d8d480b7b..24a051f9f3 100755 --- a/services/web/app/coffee/Features/Compile/CompileManager.coffee +++ b/services/web/app/coffee/Features/Compile/CompileManager.coffee @@ -37,15 +37,23 @@ module.exports = CompileManager = return callback(error) if error? for key, value of limits options[key] = value - ClsiManager.sendRequest project_id, options, (error, status, outputFiles, clsiServerId) -> + # only pass user_id down to clsi if this is a per-user compile + compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id + ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) -> return callback(error) if error? logger.log files: outputFiles, "output files" - callback(null, status, outputFiles, clsiServerId, limits) + callback(null, status, outputFiles, clsiServerId, limits, validationProblems) - deleteAuxFiles: (project_id, callback = (error) ->) -> + + stopCompile: (project_id, user_id, callback = (error) ->) -> CompileManager.getProjectCompileLimits project_id, (error, limits) -> return callback(error) if error? - ClsiManager.deleteAuxFiles project_id, limits, callback + ClsiManager.stopCompile project_id, user_id, limits, callback + + deleteAuxFiles: (project_id, user_id, callback = (error) ->) -> + CompileManager.getProjectCompileLimits project_id, (error, limits) -> + return callback(error) if error? + ClsiManager.deleteAuxFiles project_id, user_id, limits, callback getProjectCompileLimits: (project_id, callback = (error, limits) ->) -> Project.findById project_id, {owner_ref: 1}, (error, project) -> @@ -92,7 +100,7 @@ module.exports = CompileManager = else ProjectRootDocManager.setRootDocAutomatically project_id, callback - wordCount: (project_id, file, callback = (error) ->) -> + wordCount: (project_id, user_id, file, callback = (error) ->) -> CompileManager.getProjectCompileLimits project_id, (error, limits) -> return callback(error) if error? - ClsiManager.wordCount project_id, file, limits, callback + ClsiManager.wordCount project_id, user_id, file, limits, callback diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index e14b9e4582..4bcf0c671d 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout") NotificationEmailLayout = require("./Layouts/NotificationEmailLayout") settings = require("settings-sharelatex") + + templates = {} templates.registered = @@ -114,6 +116,8 @@ module.exports = template = templates[templateName] opts.siteUrl = settings.siteUrl opts.body = template.compiledTemplate(opts) + if settings.email?.templates?.customFooter? + opts.body += settings.email?.templates?.customFooter return { subject : template.subject(opts) html: template.layout(opts) diff --git a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee index 314142b3af..9f960b1d15 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee @@ -1,15 +1,15 @@ - +logger = require("logger-sharelatex") NotificationsHandler = require("./NotificationsHandler") module.exports = groupPlan: (user, licence)-> key : "join-sub-#{licence.subscription_id}" - create: (callback = ->)-> messageOpts = groupName: licence.name subscription_id: licence.subscription_id + logger.log user_id:user._id, key:key, "creating notification key for user" NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback read: (callback = ->)-> diff --git a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee index a26bfff16a..a7cf6a4672 100644 --- a/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee +++ b/services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee @@ -3,14 +3,22 @@ request = require("request") logger = require("logger-sharelatex") oneSecond = 1000 + +makeRequest = (opts, callback)-> + if !settings.apis.notifications?.url? + return callback(null, statusCode:200) + else + request(opts, callback) + module.exports = getUserNotifications: (user_id, callback)-> opts = - uri: "#{settings.apis.notifications.url}/user/#{user_id}" + uri: "#{settings.apis.notifications?.url}/user/#{user_id}" json: true timeout: oneSecond - request.get opts, (err, res, unreadNotifications)-> + method: "GET" + makeRequest opts, (err, res, unreadNotifications)-> statusCode = if res? then res.statusCode else 500 if err? or statusCode != 200 e = new Error("something went wrong getting notifications, #{err}, #{statusCode}") @@ -23,30 +31,33 @@ module.exports = createNotification: (user_id, key, templateKey, messageOpts, callback)-> opts = - uri: "#{settings.apis.notifications.url}/user/#{user_id}" + uri: "#{settings.apis.notifications?.url}/user/#{user_id}" timeout: oneSecond + method:"POST" json: { key:key messageOpts:messageOpts templateKey:templateKey } logger.log opts:opts, "creating notification for user" - request.post opts, callback + makeRequest opts, callback markAsReadWithKey: (user_id, key, callback)-> opts = - uri: "#{settings.apis.notifications.url}/user/#{user_id}" + uri: "#{settings.apis.notifications?.url}/user/#{user_id}" + method: "DELETE" timeout: oneSecond json: { key:key } logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api" - request.del opts, callback + makeRequest opts, callback markAsRead: (user_id, notification_id, callback)-> opts = - uri: "#{settings.apis.notifications.url}/user/#{user_id}/notification/#{notification_id}" + method: "DELETE" + uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}" timeout:oneSecond logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api" - request.del opts, callback + makeRequest opts, callback diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee index fb0f75beee..ec5371f0f2 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee @@ -2,6 +2,7 @@ PasswordResetHandler = require("./PasswordResetHandler") RateLimiter = require("../../infrastructure/RateLimiter") AuthenticationController = require("../Authentication/AuthenticationController") UserGetter = require("../User/UserGetter") +UserSessionsManager = require("../User/UserSessionsManager") logger = require "logger-sharelatex" module.exports = @@ -47,11 +48,13 @@ module.exports = PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) -> return next(err) if err? if found - if req.body.login_after - UserGetter.getUser user_id, {email: 1}, (err, user) -> - return next(err) if err? - AuthenticationController.doLogin {email:user.email, password: password}, req, res, next - else - res.sendStatus 200 + UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) -> + return next(err) if err? + if req.body.login_after + UserGetter.getUser user_id, {email: 1}, (err, user) -> + return next(err) if err? + AuthenticationController.doLogin {email:user.email, password: password}, req, res, next + else + res.sendStatus 200 else res.sendStatus 404 diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index f810067991..d273516056 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -245,11 +245,13 @@ module.exports = ProjectController = first_name : user.first_name last_name : user.last_name referal_id : user.referal_id + signUpDate : user.signUpDate subscription : freeTrial: {allowed: allowedFreeTrial} featureSwitches: user.featureSwitches features: user.features refProviders: user.refProviders + betaProgram: user.betaProgram } userSettings: { mode : user.ace.mode diff --git a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee index 8ba8a65845..f398ea75b5 100644 --- a/services/web/app/coffee/Features/Project/ProjectDeleter.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDeleter.coffee @@ -24,9 +24,11 @@ module.exports = ProjectDeleter = update = {deletedByExternalDataSource: false} Project.update conditions, update, {}, callback - deleteUsersProjects: (owner_id, callback)-> - logger.log owner_id:owner_id, "deleting users projects" - Project.remove owner_ref:owner_id, callback + deleteUsersProjects: (user_id, callback)-> + logger.log {user_id}, "deleting users projects" + Project.remove owner_ref:user_id, (error) -> + return callback(error) if error? + CollaboratorsHandler.removeUserFromAllProjets user_id, callback deleteProject: (project_id, callback = (error) ->) -> # archiveProject takes care of the clean-up diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 946a57d527..c68f732f16 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -512,6 +512,11 @@ module.exports = ProjectEntityHandler = return callback(e) type = sanitizeTypeOfElement type + if path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") + e = new Error("invalid element name") + logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as name was invalid" + return callback(e) + if !folder_id? folder_id = project.rootFolder[0]._id ProjectEntityHandler._countElements project, (err, count)-> diff --git a/services/web/app/coffee/Features/References/ReferencesController.coffee b/services/web/app/coffee/Features/References/ReferencesController.coffee index 0536e3b680..305c4715b5 100644 --- a/services/web/app/coffee/Features/References/ReferencesController.coffee +++ b/services/web/app/coffee/Features/References/ReferencesController.coffee @@ -15,7 +15,7 @@ module.exports = ReferencesController = return res.sendStatus 400 logger.log {projectId, docIds: docIds}, "index references for project" ReferencesHandler.index projectId, docIds, (err, data) -> - if err + if err? logger.err {err, projectId}, "error indexing all references" return res.sendStatus 500 ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data) @@ -25,12 +25,14 @@ module.exports = ReferencesController = shouldBroadcast = req.body.shouldBroadcast logger.log {projectId}, "index all references for project" ReferencesHandler.indexAll projectId, (err, data) -> - if err + if err? logger.err {err, projectId}, "error indexing all references" return res.sendStatus 500 ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data) _handleIndexResponse: (req, res, projectId, shouldBroadcast, data) -> + if !data? or !data.keys? + return res.json({projectId, keys: []}) if shouldBroadcast logger.log {projectId}, "emitting new references keys to connected clients" EditorRealTimeController.emitToRoom projectId, 'references:keys:updated', data.keys diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 7894ab6915..fa38ddedf6 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -66,6 +66,8 @@ module.exports = ReferencesHandler = ReferencesHandler._doIndexOperation(projectId, project, docIds, [], callback) _doIndexOperation: (projectId, project, docIds, fileIds, callback) -> + if !settings.apis?.references?.url? + return callback() ReferencesHandler._isFullIndex project, (err, isFullIndex) -> if err logger.err {err, projectId}, "error checking whether to do full index" diff --git a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee index 9b55b60077..e607c3a020 100644 --- a/services/web/app/coffee/Features/StaticPages/UniversityController.coffee +++ b/services/web/app/coffee/Features/StaticPages/UniversityController.coffee @@ -5,6 +5,12 @@ _ = require("underscore") ErrorController = require "../Errors/ErrorController" StaticPageHelpers = require("./StaticPageHelpers") sanitize = require('sanitizer') +Settings = require("settings-sharelatex") +contentful = require('contentful') +marked = require("marked") + + + module.exports = UniversityController = @@ -17,7 +23,7 @@ module.exports = UniversityController = logger.log url:url, "proxying request to university api" request.get universityUrl, (err, r, data)-> if r?.statusCode == 404 - return ErrorController.notFound(req, res, next) + return UniversityController.getContentfulPage(req, res, next) if err? return res.send 500 data = data.trim() @@ -37,4 +43,28 @@ module.exports = UniversityController = upstream = request.get(originUrl) upstream.on "error", (error) -> logger.error err: error, "university proxy error" - upstream.pipe res \ No newline at end of file + 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 + + + diff --git a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee index e2f633e185..6c402220a9 100644 --- a/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee +++ b/services/web/app/coffee/Features/Subscription/LimitationsManager.coffee @@ -38,12 +38,12 @@ module.exports = SubscriptionLocator.getUsersSubscription user._id, (err, subscription)-> if err? return callback(err) - hasValidSubscription = subscription? and subscription.recurlySubscription_id? + 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._ud, "checking is user is member of subscription groups" + logger.log user_id: user._id, "checking is user is member of subscription groups" SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) -> return callback(err) if err? callback err, subscriptions.length > 0, subscriptions diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee index 680905ca6c..059c5fb02c 100644 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -4,11 +4,201 @@ request = require 'request' Settings = require "settings-sharelatex" xml2js = require "xml2js" logger = require("logger-sharelatex") +Async = require('async') module.exports = RecurlyWrapper = apiUrl : "https://api.recurly.com/v2" - createSubscription: (user, subscriptionDetails, recurly_token_id, callback)-> + _addressToXml: (address) -> + allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code'] + resultString = "\n" + for k, v of address + if k == 'postal_code' + k = 'zip' + if v and (k in allowedKeys) + resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}\n" + resultString += "\n" + return resultString + + _paypal: + checkAccountExists: (cache, next) -> + user = cache.user + recurly_token_id = cache.recurly_token_id + subscriptionDetails = cache.subscriptionDetails + logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user" + RecurlyWrapper.apiRequest({ + url: "accounts/#{user._id}" + method: "GET" + }, (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) + logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly" + RecurlyWrapper._parseAccountXml responseBody, (err, account) -> + if err + logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account" + return next(err) + cache.userExists = true + cache.account = account + return next(null, cache) + ) + createAccount: (cache, next) -> + user = cache.user + recurly_token_id = cache.recurly_token_id + subscriptionDetails = cache.subscriptionDetails + address = subscriptionDetails.address + if !address + return next(new Error('no address in subscriptionDetails at createAccount stage')) + if cache.userExists + logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly" + return next(null, cache) + logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly" + requestBody = """ + + #{user._id} + #{user.email} + #{user.first_name} + #{user.last_name} +
+ #{address.address1} + #{address.address2} + #{address.city || ''} + #{address.state || ''} + #{address.zip || ''} + #{address.country} +
+
+ """ + RecurlyWrapper.apiRequest({ + url : "accounts" + method : "POST" + body : requestBody + }, (error, response, responseBody) => + if error + logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account" + return next(error) + RecurlyWrapper._parseAccountXml responseBody, (err, account) -> + if err + logger.error {err, user_id: user._id, recurly_token_id}, "error creating account" + return next(err) + cache.account = account + return next(null, cache) + ) + createBillingInfo: (cache, next) -> + user = cache.user + recurly_token_id = cache.recurly_token_id + subscriptionDetails = cache.subscriptionDetails + logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly" + accountCode = cache?.account?.account_code + if !accountCode + return next(new Error('no account code at createBillingInfo stage')) + requestBody = """ + + #{recurly_token_id} + + """ + RecurlyWrapper.apiRequest({ + url: "accounts/#{accountCode}/billing_info" + method: "POST" + body: requestBody + }, (error, response, responseBody) => + if error + logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info" + return next(error) + RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) -> + if err + logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info" + return next(err) + cache.billingInfo = billingInfo + return next(null, cache) + ) + + setAddress: (cache, next) -> + user = cache.user + recurly_token_id = cache.recurly_token_id + subscriptionDetails = cache.subscriptionDetails + logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly" + accountCode = cache?.account?.account_code + if !accountCode + return next(new Error('no account code at setAddress stage')) + address = subscriptionDetails.address + if !address + return next(new Error('no address in subscriptionDetails at setAddress stage')) + requestBody = RecurlyWrapper._addressToXml(address) + RecurlyWrapper.apiRequest({ + url: "accounts/#{accountCode}/billing_info" + method: "PUT" + body: requestBody + }, (error, response, responseBody) => + if error + logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address" + return next(error) + RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) -> + if err + logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info" + return next(err) + cache.billingInfo = billingInfo + return next(null, cache) + ) + createSubscription: (cache, next) -> + user = cache.user + recurly_token_id = cache.recurly_token_id + subscriptionDetails = cache.subscriptionDetails + logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly" + requestBody = """ + + #{subscriptionDetails.plan_code} + #{subscriptionDetails.currencyCode} + #{subscriptionDetails.coupon_code} + + #{user._id} + + + """ # TODO: check account details and billing + RecurlyWrapper.apiRequest({ + url : "subscriptions" + method : "POST" + body : requestBody + }, (error, response, responseBody) => + if error + logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription" + return next(error) + RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) -> + if err + logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription" + return next(err) + cache.subscription = subscription + return next(null, cache) + ) + + _createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) -> + logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription" + # We use `async.waterfall` to run each of these actions in sequence + # passing a `cache` object along the way. The cache is initialized + # with required data, and `async.apply` to pass the cache to the first function + cache = {user, recurly_token_id, subscriptionDetails} + Async.waterfall([ + Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache), + RecurlyWrapper._paypal.createAccount, + RecurlyWrapper._paypal.createBillingInfo, + RecurlyWrapper._paypal.setAddress, + RecurlyWrapper._paypal.createSubscription, + ], (err, result) -> + if err + logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process" + return callback(err) + if !result.subscription + err = new Error('no subscription object in result') + logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process" + return callback(err) + logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user" + callback(null, result.subscription) + ) + + _createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) -> requestBody = """ #{subscriptionDetails.plan_code} @@ -25,17 +215,23 @@ module.exports = RecurlyWrapper = """ - @apiRequest({ + RecurlyWrapper.apiRequest({ url : "subscriptions" method : "POST" body : requestBody }, (error, response, responseBody) => return callback(error) if error? - @_parseSubscriptionXml responseBody, callback - ) + RecurlyWrapper._parseSubscriptionXml responseBody, callback + ) + + createSubscription: (user, subscriptionDetails, recurly_token_id, callback)-> + isPaypal = subscriptionDetails.isPaypal + logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly" + fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription + fn user, subscriptionDetails, recurly_token_id, callback apiRequest : (options, callback) -> - options.url = @apiUrl + "/" + options.url + options.url = RecurlyWrapper.apiUrl + "/" + options.url options.headers = "Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64") "Accept" : "application/xml" @@ -60,7 +256,7 @@ module.exports = RecurlyWrapper = newAttributes[key] = value else newAttributes[newKey] = value - + return newAttributes crypto.randomBytes 32, (error, buffer) -> @@ -74,14 +270,14 @@ module.exports = RecurlyWrapper = signature = "#{signed}|#{unsignedQuery}" callback null, signature - + getSubscriptions: (accountId, callback)-> - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "accounts/#{accountId}/subscriptions" }, (error, response, body) => return callback(error) if error? - @_parseXml body, callback + RecurlyWrapper._parseXml body, callback ) @@ -94,11 +290,11 @@ module.exports = RecurlyWrapper = else url = "subscriptions/#{subscriptionId}" - @apiRequest({ + RecurlyWrapper.apiRequest({ url: url }, (error, response, body) => return callback(error) if error? - @_parseSubscriptionXml body, (error, recurlySubscription) => + RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) => return callback(error) if error? if options.includeAccount if recurlySubscription.account? and recurlySubscription.account.url? @@ -106,7 +302,7 @@ module.exports = RecurlyWrapper = else return callback "I don't understand the response from Recurly" - @getAccount accountId, (error, account) -> + RecurlyWrapper.getAccount accountId, (error, account) -> return callback(error) if error? recurlySubscription.account = account callback null, recurlySubscription @@ -124,9 +320,9 @@ module.exports = RecurlyWrapper = per_page:200 if cursor? opts.qs.cursor = cursor - @apiRequest opts, (error, response, body) => + RecurlyWrapper.apiRequest opts, (error, response, body) => return callback(error) if error? - @_parseXml body, (err, data)-> + RecurlyWrapper._parseXml body, (err, data)-> if err? logger.err err:err, "could not get accoutns" callback(err) @@ -142,19 +338,19 @@ module.exports = RecurlyWrapper = getAccount: (accountId, callback) -> - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "accounts/#{accountId}" }, (error, response, body) => return callback(error) if error? - @_parseAccountXml body, callback + RecurlyWrapper._parseAccountXml body, callback ) getBillingInfo: (accountId, callback)-> - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "accounts/#{accountId}/billing_info" }, (error, response, body) => return callback(error) if error? - @_parseXml body, callback + RecurlyWrapper._parseXml body, callback ) @@ -166,13 +362,13 @@ module.exports = RecurlyWrapper = #{options.timeframe} """ - @apiRequest({ + RecurlyWrapper.apiRequest({ url : "subscriptions/#{subscriptionId}" method : "put" body : requestBody }, (error, response, responseBody) => return callback(error) if error? - @_parseSubscriptionXml responseBody, callback + RecurlyWrapper._parseSubscriptionXml responseBody, callback ) createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)-> @@ -191,7 +387,7 @@ module.exports = RecurlyWrapper = """ logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon" - @apiRequest({ + RecurlyWrapper.apiRequest({ url : "coupons" method : "post" body : requestBody @@ -203,16 +399,16 @@ module.exports = RecurlyWrapper = lookupCoupon: (coupon_code, callback)-> - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "coupons/#{coupon_code}" }, (error, response, body) => return callback(error) if error? - @_parseXml body, callback + RecurlyWrapper._parseXml body, callback ) cancelSubscription: (subscriptionId, callback) -> logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription" - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "subscriptions/#{subscriptionId}/cancel", method: "put" }, (error, response, body) -> @@ -221,13 +417,13 @@ module.exports = RecurlyWrapper = reactivateSubscription: (subscriptionId, callback) -> logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription" - @apiRequest({ + RecurlyWrapper.apiRequest({ url: "subscriptions/#{subscriptionId}/reactivate", method: "put" }, (error, response, body) -> callback(error) ) - + redeemCoupon: (account_code, coupon_code, callback)-> requestBody = """ @@ -237,7 +433,7 @@ module.exports = RecurlyWrapper = """ logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user" - @apiRequest({ + RecurlyWrapper.apiRequest({ url : "coupons/#{coupon_code}/redeem" method : "post" body : requestBody @@ -251,7 +447,7 @@ module.exports = RecurlyWrapper = next_renewal_date = new Date() next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire) logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user" - @apiRequest({ + RecurlyWrapper.apiRequest({ url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false" method : "put" }, (error, response, responseBody) => @@ -261,7 +457,7 @@ module.exports = RecurlyWrapper = ) _parseSubscriptionXml: (xml, callback) -> - @_parseXml xml, (error, data) -> + RecurlyWrapper._parseXml xml, (error, data) -> return callback(error) if error? if data? and data.subscription? recurlySubscription = data.subscription @@ -270,7 +466,7 @@ module.exports = RecurlyWrapper = callback null, recurlySubscription _parseAccountXml: (xml, callback) -> - @_parseXml xml, (error, data) -> + RecurlyWrapper._parseXml xml, (error, data) -> return callback(error) if error? if data? and data.account? account = data.account @@ -278,6 +474,15 @@ module.exports = RecurlyWrapper = return callback "I don't understand the response from Recurly" callback null, account + _parseBillingInfoXml: (xml, callback) -> + RecurlyWrapper._parseXml xml, (error, data) -> + return callback(error) if error? + if data? and data.billing_info? + billingInfo = data.billing_info + else + return callback "I don't understand the response from Recurly" + callback null, billingInfo + _parseXml: (xml, callback) -> convertDataTypes = (data) -> if data? and data["$"]? @@ -299,7 +504,7 @@ module.exports = RecurlyWrapper = else array.push(convertDataTypes(value)) data = array - + if data instanceof Array data = (convertDataTypes(entry) for entry in data) else if typeof data == "object" @@ -315,6 +520,3 @@ module.exports = RecurlyWrapper = return callback(error) if error? result = convertDataTypes(data) callback null, result - - - diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index 2a1e576231..ca932ffab3 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -82,9 +82,9 @@ module.exports = SubscriptionController = logger.log user: user, "redirecting to plans" res.redirect "/user/subscription/plans" else - SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groups) -> + SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) -> return next(error) if error? - logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groups:groups, "showing subscription dashboard" + logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard" plans = SubscriptionViewModelBuilder.buildViewModel() res.render "subscriptions/dashboard", title: "your_subscription" @@ -92,8 +92,9 @@ module.exports = SubscriptionController = taxRate:subscription?.taxRate plans: plans subscription: subscription || {} - groups: groups + groupSubscriptions: groupSubscriptions subscriptionTabActive: true + user:user userCustomSubscriptionPage: (req, res, next)-> diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee index bdb9c41fde..e239a779b9 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee @@ -8,7 +8,7 @@ _ = require("underscore") module.exports = - buildUsersSubscriptionViewModel: (user, callback = (error, subscription, groups) ->) -> + buildUsersSubscriptionViewModel: (user, callback = (error, subscription, memberSubscriptions) ->) -> SubscriptionLocator.getUsersSubscription user, (err, subscription) -> return callback(err) if err? SubscriptionLocator.getMemberSubscriptions user, (err, memberSubscriptions = []) -> @@ -19,6 +19,7 @@ module.exports = RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)-> tax = recurlySubscription?.tax_in_cents || 0 callback null, { + admin_id:subscription.admin_id name: plan.name nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at) state: recurlySubscription?.state diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index 451729d7e9..546cea035e 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") +UserSessionsManager = require("./UserSessionsManager") UserUpdater = require("./UserUpdater") settings = require "settings-sharelatex" @@ -72,6 +73,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 UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background if err? logger.err err:err, "error populateGroupLicenceInvite" @@ -80,9 +82,12 @@ module.exports = UserController = logout : (req, res)-> metrics.inc "user.logout" logger.log user: req?.session?.user, "logging out" + sessionId = req.sessionID + user = req?.session?.user req.session.destroy (err)-> if err logger.err err: err, 'error destorying session' + UserSessionsManager.untrackSession(user, sessionId) res.redirect '/login' register : (req, res, next = (error) ->)-> @@ -116,17 +121,15 @@ module.exports = UserController = logger.log user: user, "password changed" AuthenticationManager.setUserPassword user._id, newPassword1, (error) -> return next(error) if error? - res.send - message: - type:'success' - text:'Your password has been changed' + UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) -> + return next(err) if err? + res.send + message: + type:'success' + text:'Your password has been changed' else logger.log user: user, "current password wrong" res.send message: type:'error' text:'Your old password is wrong' - - changeEmailAddress: (req, res)-> - - diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee index e9a05c46f2..ee5a426e78 100644 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -1,5 +1,6 @@ User = require("../../models/User").User UserLocator = require("./UserLocator") +logger = require("logger-sharelatex") module.exports = @@ -12,16 +13,23 @@ module.exports = self.createNewUser email:email, holdingAccount:true, callback createNewUser: (opts, callback)-> + logger.log opts:opts, "creating new user" user = new User() user.email = opts.email user.holdingAccount = opts.holdingAccount username = opts.email.match(/^[^@]*/) - if username? + if opts.first_name? and opts.first_name.length != 0 + user.first_name = opts.first_name + else if username? user.first_name = username[0] else user.first_name = "" - user.last_name = "" + + if opts.last_name? + user.last_name = opts.last_name + else + user.last_name = "" user.featureSwitches?.pdfng = true diff --git a/services/web/app/coffee/Features/User/UserHandler.coffee b/services/web/app/coffee/Features/User/UserHandler.coffee index 8af78573d6..602858eac3 100644 --- a/services/web/app/coffee/Features/User/UserHandler.coffee +++ b/services/web/app/coffee/Features/User/UserHandler.coffee @@ -6,7 +6,7 @@ logger = require("logger-sharelatex") module.exports = UserHandler = - populateGroupLicenceInvite: (user, callback)-> + populateGroupLicenceInvite: (user, callback = ->)-> logger.log user_id:user._id, "populating any potential group licence invites" licence = SubscriptionDomainHandler.getLicenceUserCanJoin user if !licence? diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 5a967605fe..567cacd35c 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -26,6 +26,7 @@ module.exports = 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) @@ -34,6 +35,7 @@ module.exports = if !user return ErrorController.notFound(req, res) if user.loginCount > 0 + logger.log user:user, "user has already logged in so is active, sending them to /login" # Already seen this user, so account must be activate # This lets users keep clicking the 'activate' link in their email # as a way to log in which, if I know our users, they will. diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index f407091122..75a9debba4 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -35,7 +35,8 @@ module.exports = UserRegistrationHandler = _createNewUserIfRequired: (user, userDetails, callback)-> if !user? - UserCreator.createNewUser {holdingAccount:false, email:userDetails.email}, callback + userDetails.holdingAccount = false + UserCreator.createNewUser {holdingAccount:false, email:userDetails.email, first_name:userDetails.first_name, last_name:userDetails.last_name}, callback else callback null, user diff --git a/services/web/app/coffee/Features/User/UserSessionsManager.coffee b/services/web/app/coffee/Features/User/UserSessionsManager.coffee new file mode 100644 index 0000000000..95974ec59a --- /dev/null +++ b/services/web/app/coffee/Features/User/UserSessionsManager.coffee @@ -0,0 +1,126 @@ +Settings = require('settings-sharelatex') +redis = require('redis-sharelatex') +logger = require("logger-sharelatex") +Async = require('async') +_ = require('underscore') + +rclient = redis.createClient(Settings.redis.web) + +module.exports = UserSessionsManager = + + _sessionSetKey: (user) -> + return "UserSessions:#{user._id}" + + # mimic the key used by the express sessions + _sessionKey: (sessionId) -> + return "sess:#{sessionId}" + + trackSession: (user, sessionId, callback=(err)-> ) -> + if !user? + logger.log {sessionId}, "no user to track, returning" + return callback(null) + if !sessionId? + logger.log {user_id: user._id}, "no sessionId to track, returning" + return callback(null) + logger.log {user_id: user._id, sessionId}, "onLogin handler" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + value = UserSessionsManager._sessionKey sessionId + rclient.multi() + .sadd(sessionSetKey, value) + .expire(sessionSetKey, "#{Settings.cookieSessionLength}") + .exec (err, response) -> + if err? + logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set" + return callback(err) + UserSessionsManager._checkSessions(user, () ->) + callback() + + untrackSession: (user, sessionId, callback=(err)-> ) -> + if !user? + logger.log {sessionId}, "no user to untrack, returning" + return callback(null) + if !sessionId? + logger.log {user_id: user._id}, "no sessionId to untrack, returning" + return callback(null) + logger.log {user_id: user._id, sessionId}, "onLogout handler" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + value = UserSessionsManager._sessionKey sessionId + rclient.multi() + .srem(sessionSetKey, value) + .expire(sessionSetKey, "#{Settings.cookieSessionLength}") + .exec (err, response) -> + if err? + logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set" + return callback(err) + UserSessionsManager._checkSessions(user, () ->) + callback() + + revokeAllUserSessions: (user, retain, callback=(err)->) -> + if !retain + retain = [] + retain = retain.map((i) -> UserSessionsManager._sessionKey(i)) + 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" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err? + logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" + return callback(err) + keysToDelete = _.filter(sessionKeys, (k) -> k not in retain) + if keysToDelete.length == 0 + logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning" + return callback(null) + logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user" + rclient.multi() + .del(keysToDelete) + .srem(sessionSetKey, keysToDelete) + .exec (err, result) -> + if err? + logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user" + return callback(err) + callback(null) + + touch: (user, callback=(err)->) -> + if !user + logger.log {}, "no user to touch sessions for, returning" + return callback(null) + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) -> + if err? + logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set" + return callback(err) + callback(null) + + _checkSessions: (user, callback=(err)->) -> + if !user + logger.log {}, "no user, returning" + return callback(null) + logger.log {user_id: user._id}, "checking sessions for user" + sessionSetKey = UserSessionsManager._sessionSetKey(user) + rclient.smembers sessionSetKey, (err, sessionKeys) -> + if err? + logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set" + return callback(err) + logger.log {user_id: user._id, count: sessionKeys.length}, "checking sessions for user" + Async.series( + sessionKeys.map( + (key) -> + (next) -> + rclient.get key, (err, val) -> + if err? + return next(err) + if !val? + logger.log {user_id: user._id, key}, ">> removing key from UserSessions set" + rclient.srem sessionSetKey, key, (err, result) -> + if err? + return next(err) + return next(null) + else + next() + ) + , (err, results) -> + logger.log {user_id: user._id}, "done checking sessions for user" + return callback(err) + ) diff --git a/services/web/app/coffee/Features/Wiki/WikiController.coffee b/services/web/app/coffee/Features/Wiki/WikiController.coffee deleted file mode 100644 index c75ee06f86..0000000000 --- a/services/web/app/coffee/Features/Wiki/WikiController.coffee +++ /dev/null @@ -1,81 +0,0 @@ -request = require("request") -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") -ErrorController = require "../Errors/ErrorController" -_ = require("underscore") -AuthenticationController = require("../Authentication/AuthenticationController") -async = require("async") -other_lngs = ["es"] - -module.exports = WikiController = - - - _checkIfLoginIsNeeded: (req, res, next)-> - if settings.apis.wiki.requireLogin - AuthenticationController.requireLogin()(req, res, next) - else - next() - - getPage: (req, res, next) -> - WikiController._checkIfLoginIsNeeded req, res, -> - - page = req.url.replace(/^\/learn/, "").replace(/^\//, "") - if page == "" - page = "Main_Page" - - logger.log page: page, "getting page from wiki" - if _.include(other_lngs, req.lng) - lngPage = "#{page}_#{req.lng}" - else - lngPage = page - jobs = - contents: (cb)-> - WikiController._getPageContent "Contents", cb - pageData: (cb)-> - WikiController._getPageContent lngPage, cb - async.parallel jobs, (error, results)-> - return next(error) if error? - {pageData, contents} = results - if pageData.content?.length > 280 - if _.include(other_lngs, req.lng) - pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) ) - WikiController._renderPage(pageData, contents, res) - else - WikiController._getPageContent page, (error, pageData) -> - return next(error) if error? - WikiController._renderPage(pageData, contents, res) - - - - - _getPageContent: (page, callback = (error, data = { content: "", title: "" }) ->) -> - request { - url: "#{settings.apis.wiki.url}/learn-scripts/api.php" - qs: { - page: decodeURI(page) - action: "parse" - format: "json" - } - }, (err, response, data)-> - return callback(err) if err? - try - data = JSON.parse(data) - catch err - logger.err err:err, data:data, "error parsing data from wiki" - result = - content: data?.parse?.text?['*'] - title: data?.parse?.title - callback null, result - - - _renderPage: (page, contents, res)-> - if page.title == "Main Page" - title = "Documentation" - else - title = page.title - - res.render "wiki/page", { - page: page - contents: contents - title: title - } \ No newline at end of file diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index faba1e4623..ee7418dacc 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -7,15 +7,19 @@ querystring = require('querystring') SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager") _ = require("underscore") Modules = require "./Modules" +Url = require "url" fingerprints = {} Path = require 'path' + + jsPath = if Settings.useMinifiedJs "/minjs/" else "/js/" + logger.log "Generating file fingerprints..." for path in [ "#{jsPath}libs/require.js", @@ -37,7 +41,18 @@ for path in [ fingerprints[path] = hash else logger.log filePath:filePath, "file does not exist for fingerprints" - + +getFingerprint = (path) -> + if fingerprints[path]? + return fingerprints[path] + else + logger.err "No fingerprint for file: #{path}" + return "" + +logger.log "Finished generating file fingerprints" + +cdnAvailable = Settings.cdn?.web?.host? +darkCdnAvailable = Settings.cdn?.web?.darkHost? module.exports = (app, webRouter, apiRouter)-> webRouter.use (req, res, next)-> @@ -45,9 +60,55 @@ module.exports = (app, webRouter, apiRouter)-> next() webRouter.use (req, res, next)-> + + 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 + 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.buildJsPath = (jsFile, opts = {})-> + path = Path.join(jsPath, jsFile) + + doFingerPrint = opts.fingerprint != false + + if !opts.qs? + opts.qs = {} + + if !opts.qs?.fingerprint? and doFingerPrint + opts.qs.fingerprint = getFingerprint(path) + + 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) + + res.locals.buildImgPath = (imgFile)-> + path = Path.join("/img/", imgFile) + return Url.resolve(staticFilesBase, path) + next() + + webRouter.use (req, res, next)-> res.locals.settings = Settings next() @@ -113,12 +174,7 @@ module.exports = (app, webRouter, apiRouter)-> next() webRouter.use (req, res, next)-> - res.locals.fingerprint = (path) -> - if fingerprints[path]? - return fingerprints[path] - else - logger.err "No fingerprint for file: #{path}" - return "" + res.locals.fingerprint = getFingerprint next() webRouter.use (req, res, next)-> diff --git a/services/web/app/coffee/infrastructure/Server.coffee b/services/web/app/coffee/infrastructure/Server.coffee index 11a3b5237e..8ac543c698 100644 --- a/services/web/app/coffee/infrastructure/Server.coffee +++ b/services/web/app/coffee/infrastructure/Server.coffee @@ -31,6 +31,7 @@ translations = require("translations-sharelatex").setup(Settings.i18n) Modules = require "./Modules" ErrorController = require "../Features/Errors/ErrorController" +UserSessionsManager = require "../Features/User/UserSessionsManager" 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) @@ -89,6 +90,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear # 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)->) next() webRouter.use ReferalConnect.use @@ -98,23 +101,23 @@ if app.get('env') == 'production' logger.info "Production Enviroment" app.enable('view cache') - - app.use (req, res, next)-> metrics.inc "http-request" crawlerLogger.log(req) next() -app.use (req, res, next) -> - if !Settings.editorIsOpen +webRouter.use (req, res, next) -> + if Settings.editorIsOpen + next() + else if req.url.indexOf("/admin") == 0 + next() + else res.status(503) res.render("general/closed", {title:"maintenance"}) - else - next() apiRouter.get "/status", (req, res)-> res.send("web sharelatex is alive") - + profiler = require "v8-profiler" apiRouter.get "/profile", (req, res) -> time = parseInt(req.query.time || "1000") diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index b0a7628f17..ba0a862fd2 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -58,6 +58,7 @@ UserSchema = new Schema mendeley: Boolean # coerce the refProviders values to Booleans zotero: Boolean } + betaProgram: { type:Boolean, default: false} conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 80c52c5a24..d6deffe755 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -30,7 +30,6 @@ PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter") StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter") ChatController = require("./Features/Chat/ChatController") BlogController = require("./Features/Blog/BlogController") -WikiController = require("./Features/Wiki/WikiController") Modules = require "./infrastructure/Modules" RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear') RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter') @@ -38,6 +37,7 @@ InactiveProjectController = require("./Features/InactiveData/InactiveProjectCont ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') +BetaProgramController = require('./Features/BetaProgram/BetaProgramController') logger = require("logger-sharelatex") _ = require("underscore") @@ -79,6 +79,7 @@ module.exports = class Router webRouter.get '/blog/*', BlogController.getPage webRouter.get '/user/activate', UserPagesController.activateAccountPage + AuthenticationController.addEndpointToLoginWhitelist '/user/activate' webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings @@ -104,7 +105,11 @@ module.exports = class Router webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings webRouter.post '/project/:Project_id/compile', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.compile - webRouter.get '/Project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf + webRouter.post '/project/:Project_id/compile/stop', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.stopCompile + + # Used by the web download buttons, adds filename header + webRouter.get '/project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf + # Used by the pdf viewers webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/, ((req, res, next) -> params = @@ -118,12 +123,36 @@ module.exports = class Router ((req, res, next) -> params = "Project_id": req.params[0] - "build": req.params[1] + "build_id": req.params[1] "file": req.params[2] req.params = params next() ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + # direct url access to output files for user but no build, to retrieve files when build fails + webRouter.get /^\/project\/([^\/]*)\/user\/([0-9a-f-]+)\/output\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "user_id": req.params[1] + "file": req.params[2] + req.params = params + next() + ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + + # direct url access to output files for a specific user and build (query string not required) + webRouter.get /^\/project\/([^\/]*)\/user\/([0-9a-f]+)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + ((req, res, next) -> + params = + "Project_id": req.params[0] + "user_id": req.params[1] + "build_id": req.params[2] + "file": req.params[3] + req.params = params + next() + ), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi + + webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncCode webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncPdf @@ -187,16 +216,13 @@ module.exports = class Router webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage - webRouter.get /learn(\/.*)?/, RateLimiterMiddlewear.rateLimit({ - endpointName: "wiki" - params: [] - maxRequests: 60 - timeInterval: 60 - }), WikiController.getPage - webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll + webRouter.get "/beta/participate", AuthenticationController.requireLogin(), BetaProgramController.optInPage + webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn + webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut + #Admin Stuff webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon @@ -216,9 +242,11 @@ module.exports = class Router apiRouter.get '/status', (req,res)-> res.send("websharelatex is up") + webRouter.get '/dev/csrf', (req, res) -> + res.send res.locals.csrfToken - webRouter.get '/health_check', HealthCheckController.check - webRouter.get '/health_check/redis', HealthCheckController.checkRedis + apiRouter.get '/health_check', HealthCheckController.check + apiRouter.get '/health_check/redis', HealthCheckController.checkRedis apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) -> sendRes = _.once (statusCode, message)-> diff --git a/services/web/app/views/admin/index.jade b/services/web/app/views/admin/index.jade index d28d88c929..0fa9017cc1 100644 --- a/services/web/app/views/admin/index.jade +++ b/services/web/app/views/admin/index.jade @@ -9,6 +9,22 @@ block content .page-header h1 Admin Panel tabset(ng-cloak) + tab(heading="System Messages") + each message in systemMessages + .alert.alert-info.row-spaced !{message.content} + hr + form(enctype='multipart/form-data', method='post', action='/admin/messages') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for="content") + input.form-control(name="content", type="text", placeholder="Message...", required) + button.btn.btn-primary(type="submit") Post Message + hr + form(enctype='multipart/form-data', method='post', action='/admin/messages/clear') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Clear all messages + + tab(heading="Open Sockets") .row-spaced ul @@ -17,7 +33,7 @@ block content ul -each agent in agents li #{agent} - + tab(heading="Close Editor") .row-spaced form(enctype='multipart/form-data', method='post',action='/admin/closeEditor') @@ -66,19 +82,6 @@ block content .form-group button.btn-primary.btn(type='submit') Poll - tab(heading="System Messages") - each message in systemMessages - .alert.alert-info.row-spaced !{message.content} - hr - form(enctype='multipart/form-data', method='post', action='/admin/messages') - input(name="_csrf", type="hidden", value=csrfToken) - .form-group - label(for="content") - input.form-control(name="content", type="text", placeholder="Message...", required) - button.btn.btn-primary(type="submit") Post Message - hr - form(enctype='multipart/form-data', method='post', action='/admin/messages/clear') - input(name="_csrf", type="hidden", value=csrfToken) - button.btn.btn-danger(type="submit") Clear all messages + diff --git a/services/web/app/views/beta_program/opt_in.jade b/services/web/app/views/beta_program/opt_in.jade new file mode 100644 index 0000000000..c6411cb211 --- /dev/null +++ b/services/web/app/views/beta_program/opt_in.jade @@ -0,0 +1,47 @@ +extends ../layout + +block content + .content.content-alt + .container.beta-opt-in-wrapper + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card + .page-header.text-centered + h1 + | #{translate("sharelatex_beta_program")} + .beta-opt-in + .container-fluid + .row + .col-md-12 + p.text-centered #{translate("beta_program_benefits")} + p.text-centered + | #{translate("beta_program_badge_description")} + span.beta-feature-badge + p.text-centered + | #{translate("beta_program_current_beta_features_description")} + ul.list-unstyled.text-center + li + i.fa.fa-fw.fa-book + |  #{translate("mendeley_integration")} + .row.text-centered + .col-md-12 + if user.betaProgram + p #{translate("beta_program_already_participating")} + form(id="beta-program-opt-out", method="post", action="/beta/opt-out", novalidate) + .form-group + input(type="hidden", name="_csrf", value=csrfToken) + button.btn.btn-primary( + type="submit" + ) + span #{translate("beta_program_opt_out_action")} + .form-group + a(href="/project").btn.btn-info #{translate("back_to_your_projects")} + else + form(id="beta-program-opt-in", method="post", action="/beta/opt-in", novalidate) + .form-group + input(type="hidden", name="_csrf", value=csrfToken) + button.btn.btn-primary( + type="submit" + ) + span #{translate("beta_program_opt_in_action")} + diff --git a/services/web/app/views/contact-us-modal.jade b/services/web/app/views/contact-us-modal.jade index 46090eec1b..f4fd8938b7 100644 --- a/services/web/app/views/contact-us-modal.jade +++ b/services/web/app/views/contact-us-modal.jade @@ -11,7 +11,19 @@ script(type='text/ng-template', id='supportModalTemplate') label | #{translate("subject")} .form-group - input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='') + input.field.text.medium.span8.form-control( + ng-model="form.subject", + ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }" + maxlength='255', + tabindex='1', + onkeyup='') + .contact-suggestions(ng-show="suggestions.length") + p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "__kb__", kb: translate("knowledge_base") })} + ul.contact-suggestion-list + li(ng-repeat="suggestion in suggestions") + a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") + span(ng-bind-html="suggestion.name") + i.fa.fa-angle-right label.desc(ng-show="'#{getUserEmail()}'.length < 1") | #{translate("email")} .form-group(ng-show="'#{getUserEmail()}'.length < 1") @@ -21,7 +33,7 @@ script(type='text/ng-template', id='supportModalTemplate') .form-group input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='') label.desc - | #{translate("suggestion")} + | #{translate("contact_message_label")} .form-group textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='') .form-group.text-center diff --git a/services/web/app/views/general/500.jade b/services/web/app/views/general/500.jade index e045a7d457..ef74ed7435 100644 --- a/services/web/app/views/general/500.jade +++ b/services/web/app/views/general/500.jade @@ -3,7 +3,7 @@ html(itemscope, itemtype='http://schema.org/Product') head title Something went wrong link(rel="icon", href="/favicon.ico") - link(rel='stylesheet', href='/stylesheets/style.css') + link(rel='stylesheet', href=buildCssPath('/style.css')) link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") body .content @@ -12,7 +12,7 @@ html(itemscope, itemtype='http://schema.org/Product') .col-md-8.col-md-offset-2.text-center .page-header h2 Oh dear, something went wrong. - p: img(src="/img/lion-sad-128.png", alt="Sad Lion") + p: img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion") p | Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail} p diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index ae0e7550be..83d7da4838 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -13,13 +13,12 @@ html(itemscope, itemtype='http://schema.org/Product') -if (typeof(title) == "undefined") - title ShareLaTeX, the Online LaTeX Editor + title= 'ShareLaTeX, '+ translate("online_latex_editor") -else - title= translate(title) + ' - ShareLaTeX, '+translate("online_latex_editor") + title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor") link(rel="icon", href="/favicon.ico") - link(rel='stylesheet', href='/stylesheets/style.css?fingerprint='+fingerprint('/stylesheets/style.css')) - link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") + link(rel='stylesheet', href=buildCssPath('/style.css')) if settings.i18n.subdomainLang each subdomainDetails in settings.i18n.subdomainLang @@ -30,7 +29,7 @@ html(itemscope, itemtype='http://schema.org/Product') meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor") -if (typeof(meta) == "undefined") - meta(itemprop="description", name="description", content="An online LaTeX editor that's easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.") + meta(itemprop="description", name="description", content='#{translate("site_description")}') -else meta(itemprop="description", name="description" , content=meta) @@ -46,15 +45,84 @@ html(itemscope, itemtype='http://schema.org/Product') ga('send', 'pageview'); - else script(type='text/javascript'). - window.ga = function() { console.log("Sending to GA", arguments) }; + 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="#{jsPath}libs/jquery-1.11.1.min.js") - script(src="#{jsPath}libs/angular-1.3.15.min.js") - include sentry + script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false})) + script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false})) script. window.sharelatex = { siteUrl: '#{settings.siteUrl}', @@ -123,12 +191,14 @@ html(itemscope, itemtype='http://schema.org/Product') } }; script( - data-main=jsPath+'main.js', - baseurl=jsPath, - src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') + data-main=buildJsPath('main.js', {fingerprint:false}), + baseurl=fullJsPath, + src=buildJsPath('libs/require.js') ) + include contact-us-modal - + include sentry + diff --git a/services/web/app/views/layout/footer.jade b/services/web/app/views/layout/footer.jade index 8c9652ecc5..efd64b6f6e 100644 --- a/services/web/app/views/layout/footer.jade +++ b/services/web/app/views/layout/footer.jade @@ -15,7 +15,7 @@ footer.site-footer aria-expanded="false", tooltip="#{translate('language')}" ) - img(src="/img/flags/24/#{currentLngCode}.png") + figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}") ul.dropdown-menu(role="menu") li.dropdown-header #{translate("language")} @@ -23,9 +23,9 @@ footer.site-footer if !subdomainDetails.hide li.lngOption a.menu-indent(href=subdomainDetails.url+currentUrl) - img(src="/img/flags/24/#{subdomainDetails.lngCode}.png") + figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}") | #{translate(subdomainDetails.lngCode)} - + //- img(src="/img/flags/24/.png") each item in nav.left_footer li if item.url @@ -41,4 +41,3 @@ footer.site-footer a(href=item.url, class=item.class) !{item.text} else | !{item.text} - diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 89ff4e1dc2..b6b3c61741 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -3,7 +3,9 @@ nav.navbar.navbar-default .navbar-header button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}") i.fa.fa-bars - if nav.title + if settings.nav.custom_logo + a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand + else if (nav.title) a(href='/').navbar-title #{nav.title} else a(href='/').navbar-brand @@ -17,6 +19,8 @@ nav.navbar.navbar-default | Admin b.caret ul.dropdown-menu + li + a(href="/admin") Manage Site li a(href="/admin/user") Manage Users diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index e666724b09..1e36e374f1 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -97,9 +97,10 @@ block content window.csrfToken = "!{csrfToken}"; window.anonymous = #{anonymous}; window.maxDocLength = #{maxDocLength}; + window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { - "mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML", + "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" }, @@ -128,15 +129,18 @@ block content - var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js' - var fingerprintedPath = fingerprint(jsPath+pdfPath) - - var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath + - var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn + + script(type='text/javascript'). window.pdfJsWorkerPath = "#{pdfJsWorkerPath}"; script( - data-main=jsPath+"ide.js", - baseurl=jsPath, - data-ace-base=jsPath+'ace', - src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') + data-main=buildJsPath("ide.js", {fingerprint:false}), + baseurl=fullJsPath, + data-ace-base=buildJsPath('ace', {fingerprint:false}), + src=buildJsPath('libs/require.js') ) + diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index d6c0876f96..ea5efff304 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -35,7 +35,7 @@ div.full-size( resize-on="layout:main:resize,layout:pdf:resize", annotations="pdf.logEntryAnnotations[editor.open_doc_id]", read-only="!permissions.write", - on-ctrl-enter="recompile" + on-ctrl-enter="recompileViaKey" ) .ui-layout-east diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index 52ea62d505..05fa19542f 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -15,7 +15,7 @@ aside#left-menu.full-size( | #{translate("source")} li a( - ng-href="{{pdf.url}}" + ng-href="{{pdf.downloadUrl || pdf.url}}" target="_blank" ng-if="pdf.url" ) diff --git a/services/web/app/views/project/editor/pdf.jade b/services/web/app/views/project/editor/pdf.jade index df97ff889e..200c609d97 100644 --- a/services/web/app/views/project/editor/pdf.jade +++ b/services/web/app/views/project/editor/pdf.jade @@ -1,6 +1,13 @@ div.full-size.pdf(ng-controller="PdfController") .toolbar.toolbar-tall - .btn-group(dropdown) + .btn-group( + dropdown, + tooltip-html="'#{translate('recompile_pdf')} ({{modifierKey}} + Enter)'" + tooltip-class="keyboard-tooltip" + tooltip-popup-delay="500" + tooltip-append-to-body="true" + tooltip-placement="bottom" + ) a.btn.btn-info( href, ng-disabled="pdf.compiling", @@ -29,6 +36,14 @@ div.full-size.pdf(ng-controller="PdfController") i.fa.fa-fw(ng-class="{'fa-check': draft}") |  #{translate("fast")}  span.subdued [draft] + a( + href + ng-click="stop()" + ng-show="pdf.compiling", + tooltip="#{translate('stop_compile')}" + tooltip-placement="bottom" + ) + i.fa.fa-stop() a.log-btn( href ng-click="toggleLogs()" @@ -46,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController") ) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }} a( - ng-href="{{pdf.url}}" + ng-href="{{pdf.downloadUrl || pdf.url}}" target="_blank" ng-if="pdf.url" tooltip="#{translate('download_pdf')}" @@ -93,15 +108,98 @@ div.full-size.pdf(ng-controller="PdfController") 'alert-info': entry.level == 'typesetting'\ }" ng-click="openInEditor(entry)" + ng-init="feedbackSent = false; showNegFeedbackUI = false; negFeedbackReason = ''; negFeedbackReasonFreeText = ''" ) span.line-no + i.fa.fa-link(aria-hidden="true") + |   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-content(ng-show="entry.content") {{ entry.content }} + .card.card-hint( + ng-if="entry.humanReadableHint" + stop-propagation="click" + ) + figure.card-hint-icon-container + i.fa.fa-lightbulb-o(aria-hidden="true") + p.card-hint-text( + ng-show="entry.humanReadableHint", + ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)") + .card-hint-actions.clearfix + .card-hint-ext-link(ng-if="wikiEnabled") + a( + ng-href="{{ entry.extraInfoURL }}", + ng-click="trackLogHintsLearnMore()" + target="_blank" + ) + i.fa.fa-external-link + | #{translate("log_hint_extra_info")} + .card-hint-feedback( + ng-hide="feedbackSent || showNegFeedbackUI" + ng-class="entry.ruleId" + ) + label.card-hint-feedback-label #{translate("log_hint_feedback_label")} + a.card-hint-feedback-positive( + ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;" + href + ) #{translate("answer_yes")} + span  /  + a.card-hint-feedback-negative( + ng-click="trackLogHintsNegativeFeedback(entry.ruleId); showNegFeedbackUI = true;" + href + ) #{translate("answer_no")} + .card-hint-extra-feedback(ng-hide="!showNegFeedbackUI || feedbackSent") + p.card-hint-extra-feedback-label #{translate("log_hint_ask_extra_feedback")} + .radio: label + input( + type="radio" + name="{{ 'neg-feedback-reason-' + $index }}" + ng-model="negFeedbackReason" + value="{{ logHintsNegFeedbackValues.DIDNT_UNDERSTAND }}" + ) + | #{translate("log_hint_extra_feedback_didnt_understand")} + .radio: label + input( + type="radio" + name="{{ 'neg-feedback-reason-' + $index }}" + ng-model="negFeedbackReason" + value="{{ logHintsNegFeedbackValues.NOT_APPLICABLE }}" + ) + | #{translate("log_hint_extra_feedback_not_applicable")} + .radio: label + input( + type="radio" + name="{{ 'neg-feedback-reason-' + $index }}" + ng-model="negFeedbackReason" + value="{{ logHintsNegFeedbackValues.INCORRECT }}" + ) + | #{translate("log_hint_extra_feedback_incorrect")} + .radio: label + input( + type="radio" + name="{{ 'neg-feedback-reason-' + $index }}" + ng-model="negFeedbackReason" + value="{{ logHintsNegFeedbackValues.OTHER }}" + ) + | #{translate("log_hint_extra_feedback_other")} + textarea.form-control( + ng-show="negFeedbackReason === logHintsNegFeedbackValues.OTHER" + ng-model="negFeedbackReasonFreeText" + rows="2" + ) + .clearfix + button.btn.btn-default.btn-sm.pull-right( + ng-disabled="!negFeedbackReason" + ng-click="trackLogHintsNegFeedbackDetails(entry.ruleId, negFeedbackReason, negFeedbackReasonFreeText); feedbackSent = true;" + ) #{translate("log_hint_extra_feedback_submit")} + + .card-hint-feedback(ng-show="feedbackSent") + label.card-hint-feedback-label #{translate("log_hint_feedback_gratitude")} + + p.entry-content(ng-show="entry.content") {{ entry.content.trim() }} p - .pull-right + .files-dropdown-container a.btn.btn-default.btn-sm( href, tooltip="#{translate('clear_cached_files')}", @@ -111,12 +209,15 @@ div.full-size.pdf(ng-controller="PdfController") ) i.fa.fa-trash-o |   - div.dropdown(style="display: inline-block;", dropdown) + div.files-dropdown( + ng-class="shouldDropUp ? 'dropup' : 'dropdown'" + dropdown + ) a.btn.btn-default.btn-sm( href dropdown-toggle ) - | !{translate("other_logs_and_files")} + | !{translate("other_logs_and_files")} span.caret ul.dropdown-menu.dropdown-menu-right li(ng-repeat="file in pdf.outputFiles") @@ -154,6 +255,27 @@ div.full-size.pdf(ng-controller="PdfController") ng-if="settings.pdfViewer == 'native'" ) + .pdf-validation-problems(ng-switch-when="validation-problems") + + .alert.alert-danger(ng-show="pdf.validation.duplicatePaths") + strong #{translate("latex_error")} + span #{translate("duplicate_paths_found")} + + + .alert.alert-danger(ng-show="pdf.validation.sizeCheck") + strong #{translate("project_too_large")} + div #{translate("project_too_large_please_reduce")} + div + li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb + + .alert.alert-danger(ng-show="pdf.validation.conflictedPaths") + div + strong #{translate("conflicting_paths_found")} + div #{translate("following_paths_conflict")} + div + li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }} + + .pdf-errors(ng-switch-when="errors") .alert.alert-danger(ng-show="pdf.error") @@ -171,7 +293,11 @@ div.full-size.pdf(ng-controller="PdfController") .alert.alert-danger(ng-show="pdf.tooRecentlyCompiled") strong #{translate("server_error")} span #{translate("too_recently_compiled")} - + + .alert.alert-danger(ng-show="pdf.compileTerminated") + strong #{translate("terminated")}. + span #{translate("compile_terminated_by_user")} + .alert.alert-danger(ng-show="pdf.timedout") p strong #{translate("timedout")}. diff --git a/services/web/app/views/project/editor/share.jade b/services/web/app/views/project/editor/share.jade index dad1d42108..ad3b2bd9ba 100644 --- a/services/web/app/views/project/editor/share.jade +++ b/services/web/app/views/project/editor/share.jade @@ -49,7 +49,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate') .form-group tags-input( template="shareTagTemplate" - placeholder="joe@example.com, sue@example.com, ..." + placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}" ng-model="inputs.contacts" focus-on="open" display-property="display" diff --git a/services/web/app/views/project/list/side-bar.jade b/services/web/app/views/project/list/side-bar.jade index a30127840a..96365c813e 100644 --- a/services/web/app/views/project/list/side-bar.jade +++ b/services/web/app/views/project/list/side-bar.jade @@ -94,18 +94,7 @@ - if (showUserDetailsArea) span(ng-controller="LeftHandMenuPromoController", ng-cloak) - .row-spaced(ng-if="showDatajoy") - hr - .card.card-thin - p.text-center.small - | Python or R user? - p.text-center.small - a(href="https://www.getdatajoy.com/", target="_blank").btn.btn-info.btn-small Try DataJoy - p.text-center.small(style="font-size: 0.8em") - a(href="https://www.getdatajoy.com/", target="_blank") DataJoy - | is a new online Python and R editor from ShareLaTeX. - - .row-spaced#userProfileInformation(ng-if="hasProjects && !showDatajoy") + .row-spaced#userProfileInformation(ng-if="hasProjects") div(ng-controller="UserProfileController") hr(ng-show="percentComplete < 100") .text-centered.user-profile(ng-show="percentComplete < 100") @@ -120,7 +109,7 @@ ) #{translate("complete")} - .row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered + .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered span(sixpack-default).text-centered hr p.small #{translate("on_free_sl")} @@ -146,7 +135,7 @@ p span Get Dropbox Sync p - img(src="/img/dropbox/simple_logo.png") + img(src=buildImgPath("dropbox/simple_logo.png")) p a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} p.small.text-centered @@ -159,13 +148,13 @@ p span Get Github Sync p - img(src="/img/github/octocat.jpg") + img(src=buildImgPath("github/octocat.jpg")) p a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} p.small.text-centered | #{translate("or_unlock_features_bonus")} a(href="/user/bonus") #{translate("sharing_sl")} . script. - window.userHasSubscription = #{settings.enableSubscriptions && !hasSubscription} + window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription} diff --git a/services/web/app/views/sentry.jade b/services/web/app/views/sentry.jade index 9fe5dd3b7d..4d838cd698 100644 --- a/services/web/app/views/sentry.jade +++ b/services/web/app/views/sentry.jade @@ -2,7 +2,7 @@ - if (sentrySrc.match(/^([a-z]+:)?\/\//i)) script(src="#{sentrySrc}") - else - script(src="#{jsPath}libs/#{sentrySrc}") + script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false})) - if (typeof(sentrySrc) != "undefined") script(type="text/javascript"). if (typeof(Raven) != "undefined" && Raven.config) { diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade index dd3e600aa1..9b9464419e 100644 --- a/services/web/app/views/subscriptions/dashboard.jade +++ b/services/web/app/views/subscriptions/dashboard.jade @@ -43,12 +43,8 @@ block content .card(ng-if="view == 'overview'") .page-header h1 #{translate("your_subscription")} - -if (groups.length != 0) - each groupSubscription in groups - p !{translate("member_of_group_subscription", {admin_email: "" + groupSubscription.admin_id.email + ""})} - span - button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")} - - else if (subscription) + + - if (subscription && user._id+'' == subscription.admin_id+'') case subscription.state when "free-trial" p !{translate("on_free_trial_expiring_at", {expiresAt:"" + subscription.expiresAt + ""})} @@ -72,15 +68,9 @@ block content p !{translate("your_subscription_has_expired")} a(href="/user/subscription/plans") !{translate("create_new_subscription")} default - -if(groups.length == 0) + -if(groupSubscriptions.length == 0) p !{translate("problem_with_subscription_contact_us")} - -if(subscription.groupPlan) - a(href="/subscription/group").btn.btn-primary !{translate("manage_group")} - - - - div(ng-show="changePlan", ng-cloak)#changePlanSection h2.col-md-7 !{translate("change_plan")} span.dropdown.col-md-1.changePlanButton(ng-controller="CurrenyDropdownController", style="display:none", dropdown) @@ -105,6 +95,20 @@ block content mixin printPlans(plans.individualMonthlyPlans) mixin printPlans(plans.individualAnnualPlans) + + each groupSubscription in groupSubscriptions + - if (user._id+'' != groupSubscription.admin_id._id+'') + div + p !{translate("member_of_group_subscription", {admin_email: "" + groupSubscription.admin_id.email + ""})} + span + button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")} + + -if(subscription.groupPlan && user._id+'' == subscription.admin_id+'') + div + a(href="/subscription/group").btn.btn-primary !{translate("manage_group")} + + + .card(ng-if="view == 'cancelation'") .page-header h1 #{translate("Cancel Subscription")} diff --git a/services/web/app/views/subscriptions/edit-billing-details.jade b/services/web/app/views/subscriptions/edit-billing-details.jade index bc47efb4cb..f0b6671bcd 100644 --- a/services/web/app/views/subscriptions/edit-billing-details.jade +++ b/services/web/app/views/subscriptions/edit-billing-details.jade @@ -3,7 +3,7 @@ extends ../layout block content - locals.supressDefaultJs = true script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) - script(src=jsPath+'libs/recurly.min.js') + script(src=buildJsPath('libs/recurly.min.js')) .content.content-alt .container diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.jade index 86b0263238..8413f5e036 100644 --- a/services/web/app/views/subscriptions/successful_subscription.jade +++ b/services/web/app/views/subscriptions/successful_subscription.jade @@ -30,10 +30,10 @@ block content | Henry and James .portraits span.img-circle - img(src="/img/about/henry_oswald.jpg") + img(src=buildImgPath("about/henry_oswald.jpg")) |   span.img-circle - img(src="/img/about/james_allen.jpg") + img(src=buildImgPath("about/james_allen.jpg")) p a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")} diff --git a/services/web/app/views/translations/translation_message.jade b/services/web/app/views/translations/translation_message.jade index 333139139f..225ad3ea2c 100644 --- a/services/web/app/views/translations/translation_message.jade +++ b/services/web/app/views/translations/translation_message.jade @@ -2,7 +2,7 @@ span(ng-controller="TranslationsPopupController", ng-cloak) .translations-message(ng-hide="hidei18nNotification") a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"" + translate(recomendSubdomain.lngCode) + ""})} - img(src="/img/flags/24/#{recomendSubdomain.lngCode}.png") + img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png")) button(ng-click="dismiss()").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} \ No newline at end of file diff --git a/services/web/app/views/university/case_study.jade b/services/web/app/views/university/case_study.jade new file mode 100644 index 0000000000..7974eb9e6c --- /dev/null +++ b/services/web/app/views/university/case_study.jade @@ -0,0 +1,19 @@ +extends ../layout + +block content + .masthead + .container + .row + .col-md-12 + h1 !{viewData.title} + .row.row-spaced + + .pattern-container + .content + .container + .row + .col-md-10.col-md-offset-1 + .card + .row + | !{viewData.html} + diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade index 5d25d80c1b..a23c3660aa 100644 --- a/services/web/app/views/user/settings.jade +++ b/services/web/app/views/user/settings.jade @@ -99,6 +99,17 @@ block content | !{moduleIncludes("userSettings", locals)} + 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")} + hr if !externalAuthenticationSystemUsed() diff --git a/services/web/app/views/wiki/page.jade b/services/web/app/views/wiki/page.jade deleted file mode 100644 index 6a6bea2202..0000000000 --- a/services/web/app/views/wiki/page.jade +++ /dev/null @@ -1,68 +0,0 @@ -extends ../layout - -block content - .content.content-alt(ng-cloak) - .container.wiki - .row.template-page-header - .col-md-8(ng-cloak) - - .row - .col-xs-3.contents(ng-non-bindable) - | !{contents.content} - - .col-xs-9.page - - if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined") - span(ng-controller="SearchWikiController") - .row - form.project-search.form-horizontal.col-md-9(role="form") - .form-group.has-feedback.has-feedback-left.col-md-12 - input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....") - i.fa.fa-search.form-control-feedback-left - i.fa.fa-times.form-control-feedback( - ng-click="clearSearchText()", - style="cursor: pointer;", - ng-show="searchQueryText.length > 0" - ) - .col-md-3.text-right - a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("suggest_new_doc")} - - .row - .col-md-12(ng-cloak) - a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin - span(ng-bind-html='hit.name') - div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content') - - .card.row-spaced(ng-non-bindable) - .page-header - h1 #{title} - - | !{page.content} - - - - - script(type="text/ng-template", id="missingWikiPageModal") - .modal-header - button.close( - type="button" - data-dismiss="modal" - ng-click="close()" - ) × - h3 #{translate("suggest_new_doc")} - .modal-body.contact-us-modal - span(ng-show="sent == false") - label.desc - | #{translate("email")} (#{translate("optional")}) - .form-group - input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2') - label.desc - | #{translate("suggestion")} - .form-group - textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='') - span(ng-show="sent") - p #{translate("request_sent_thank_you")} - .modal-footer - button.btn.btn-default(ng-click="close()") - span #{translate("dismiss")} - button.btn-success.btn(type='submit', ng-disabled="sending", ng-click="contactUs()") #{translate("contact_us")} - diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index beb933e4c9..f345d14129 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -15,7 +15,7 @@ httpAuthUsers[httpAuthUser] = httpAuthPass sessionSecret = "secret-please-change" -module.exports = +module.exports = settings = # File storage # ------------ # @@ -104,16 +104,21 @@ module.exports = url: "http://localhost:3036" sixpack: url: "" - references: - url: "http://localhost:3040" - notifications: - url: "http://localhost:3042" + # references: + # url: "http://localhost:3040" + # notifications: + # url: "http://localhost:3042" templates: user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2" showSocialButtons: false showComments: false + # cdn: + # web: + # host:"http://cdn.sharelatex.dev:3000" + # darkHost:"http://cdn.sharelatex.dev:3000" + # Where your instance of ShareLaTeX can be found publically. Used in emails # that are sent out, generated links, etc. siteUrl : siteUrl = 'http://localhost:3000' @@ -137,6 +142,7 @@ module.exports = # -------- security: sessionSecret: sessionSecret + bcryptRounds: 12 # number of rounds used to hash user passwords (raised to power 2) httpAuthUsers: httpAuthUsers @@ -261,6 +267,10 @@ module.exports = # Should we allow access to any page without logging in? This includes # public projects, /learn, /templates, about pages, etc. allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false + + # Use a single compile directory for all users in a project + # (otherwise each user has their own directory) + # disablePerUserCompiles: true # Maximum size of text documents in the real-time editing system. max_doc_length: 2 * 1024 * 1024 # 2mb @@ -334,7 +344,9 @@ module.exports = url: "/logout" }] }] - + + customisation: {} + # templates: [{ # name : "cv_or_resume", # url : "/templates/cv" diff --git a/services/web/npm-shrinkwrap.json b/services/web/npm-shrinkwrap.json new file mode 100644 index 0000000000..89d5dfe8bb --- /dev/null +++ b/services/web/npm-shrinkwrap.json @@ -0,0 +1,3853 @@ +{ + "name": "web-sharelatex", + "version": "0.1.4", + "dependencies": { + "archiver": { + "version": "0.9.0", + "from": "archiver@0.9.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-0.9.0.tgz", + "dependencies": { + "buffer-crc32": { + "version": "0.2.5", + "from": "buffer-crc32@~0.2.1", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "tar-stream": { + "version": "0.3.3", + "from": "tar-stream@~0.3.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-0.3.3.tgz", + "dependencies": { + "bl": { + "version": "0.6.0", + "from": "bl@~0.6.0", + "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", + "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", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + } + } + } + } + }, + "zip-stream": { + "version": "0.3.7", + "from": "zip-stream@~0.3.0", + "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", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-0.2.0.tgz" + }, + "debug": { + "version": "1.0.4", + "from": "debug@~1.0.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", + "dependencies": { + "ms": { + "version": "0.6.2", + "from": "ms@0.6.2", + "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", + "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", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-0.1.0.tgz" + }, + "file-utils": { + "version": "0.1.5", + "from": "file-utils@~0.1.5", + "resolved": "https://registry.npmjs.org/file-utils/-/file-utils-0.1.5.tgz", + "dependencies": { + "lodash": { + "version": "2.1.0", + "from": "lodash@~2.1.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.1.0.tgz" + }, + "iconv-lite": { + "version": "0.2.11", + "from": "iconv-lite@~0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" + }, + "glob": { + "version": "3.2.11", + "from": "glob@~3.2.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "minimatch": { + "version": "0.3.0", + "from": "minimatch@0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + } + } + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@~0.2.12", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + }, + "findup-sync": { + "version": "0.1.3", + "from": "findup-sync@~0.1.2", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "dependencies": { + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + } + } + }, + "isbinaryfile": { + "version": "0.1.9", + "from": "isbinaryfile@~0.1.9", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-0.1.9.tgz" + } + } + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + } + } + }, + "async": { + "version": "0.6.2", + "from": "async@0.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.6.2.tgz" + }, + "base64-stream": { + "version": "0.1.3", + "from": "base64-stream@^0.1.2", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + } + }, + "basic-auth-connect": { + "version": "1.0.0", + "from": "basic-auth-connect@^1.0.0", + "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", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-0.8.3.tgz", + "dependencies": { + "bindings": { + "version": "1.2.1", + "from": "bindings@1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" + }, + "nan": { + "version": "1.8.4", + "from": "nan@1.8.4", + "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", + "dependencies": { + "bytes": { + "version": "2.3.0", + "from": "bytes@2.3.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" + }, + "content-type": { + "version": "1.0.2", + "from": "content-type@~1.0.1", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "depd": { + "version": "1.1.0", + "from": "depd@~1.1.0", + "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", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "statuses": { + "version": "1.3.0", + "from": "statuses@>= 1.2.1 < 2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" + } + } + }, + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + }, + "on-finished": { + "version": "2.3.0", + "from": "on-finished@~2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "dependencies": { + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "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" + }, + "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", + "dependencies": { + "unpipe": { + "version": "1.0.0", + "from": "unpipe@1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + } + } + }, + "type-is": { + "version": "1.6.13", + "from": "type-is@~1.6.12", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + } + } + } + } + } + } + }, + "bufferedstream": { + "version": "1.6.0", + "from": "bufferedstream@1.6.0", + "resolved": "https://registry.npmjs.org/bufferedstream/-/bufferedstream-1.6.0.tgz" + }, + "connect-redis": { + "version": "2.3.0", + "from": "connect-redis@2.3.0", + "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-2.3.0.tgz", + "dependencies": { + "debug": { + "version": "1.0.4", + "from": "debug@^1.0.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.4.tgz", + "dependencies": { + "ms": { + "version": "0.6.2", + "from": "ms@0.6.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz" + } + } + }, + "redis": { + "version": "0.12.1", + "from": "redis@^0.12.1", + "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", + "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", + "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" + }, + "regenerator-runtime": { + "version": "0.9.5", + "from": "regenerator-runtime@^0.9.5", + "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", + "dependencies": { + "follow-redirects": { + "version": "0.0.7", + "from": "follow-redirects@0.0.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", + "dependencies": { + "debug": { + "version": "2.2.0", + "from": "debug@^2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "stream-consume": { + "version": "0.1.0", + "from": "stream-consume@^0.1.0", + "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" + } + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@^5.0.1", + "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", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.2.4.tgz" + }, + "cookie-parser": { + "version": "1.3.5", + "from": "cookie-parser@1.3.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", + "dependencies": { + "cookie": { + "version": "0.1.3", + "from": "cookie@0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" + }, + "cookie-signature": { + "version": "1.0.6", + "from": "cookie-signature@1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + } + } + }, + "csurf": { + "version": "1.9.0", + "from": "csurf@^1.8.3", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz", + "dependencies": { + "cookie": { + "version": "0.3.1", + "from": "cookie@0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz" + }, + "cookie-signature": { + "version": "1.0.6", + "from": "cookie-signature@1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + }, + "csrf": { + "version": "3.0.3", + "from": "csrf@~3.0.3", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.3.tgz", + "dependencies": { + "base64-url": { + "version": "1.2.2", + "from": "base64-url@1.2.2", + "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.2.tgz" + }, + "rndm": { + "version": "1.2.0", + "from": "rndm@1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz" + }, + "tsscmp": { + "version": "1.0.5", + "from": "tsscmp@1.0.5", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz" + }, + "uid-safe": { + "version": "2.1.1", + "from": "uid-safe@2.1.1", + "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", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz" + } + } + } + } + }, + "http-errors": { + "version": "1.5.0", + "from": "http-errors@~1.5.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.0.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "setprototypeof": { + "version": "1.0.1", + "from": "setprototypeof@1.0.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.1.tgz" + }, + "statuses": { + "version": "1.3.0", + "from": "statuses@>= 1.3.0 < 2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" + } + } + } + } + }, + "dateformat": { + "version": "1.0.4-1.2.3", + "from": "dateformat@1.0.4-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.4-1.2.3.tgz" + }, + "express": { + "version": "4.13.0", + "from": "express@4.13.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.13.0.tgz", + "dependencies": { + "accepts": { + "version": "1.2.13", + "from": "accepts@~1.2.9", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", + "dependencies": { + "mime-types": { + "version": "2.1.11", + "from": "mime-types@~2.1.11", + "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", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + } + } + }, + "negotiator": { + "version": "0.5.3", + "from": "negotiator@0.5.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz" + } + } + }, + "array-flatten": { + "version": "1.1.0", + "from": "array-flatten@1.1.0", + "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", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz" + }, + "content-type": { + "version": "1.0.2", + "from": "content-type@~1.0.1", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz" + }, + "cookie": { + "version": "0.1.3", + "from": "cookie@0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" + }, + "cookie-signature": { + "version": "1.0.6", + "from": "cookie-signature@1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "depd": { + "version": "1.0.1", + "from": "depd@~1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz" + }, + "escape-html": { + "version": "1.0.2", + "from": "escape-html@1.0.2", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz" + }, + "etag": { + "version": "1.7.0", + "from": "etag@~1.7.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz" + }, + "finalhandler": { + "version": "0.4.0", + "from": "finalhandler@0.4.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", + "dependencies": { + "unpipe": { + "version": "1.0.0", + "from": "unpipe@~1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + } + } + }, + "fresh": { + "version": "0.3.0", + "from": "fresh@0.3.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz" + }, + "merge-descriptors": { + "version": "1.0.0", + "from": "merge-descriptors@1.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz" + }, + "methods": { + "version": "1.1.2", + "from": "methods@~1.1.1", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + }, + "on-finished": { + "version": "2.3.0", + "from": "on-finished@~2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "dependencies": { + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + } + } + }, + "parseurl": { + "version": "1.3.1", + "from": "parseurl@~1.3.0", + "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", + "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", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", + "dependencies": { + "forwarded": { + "version": "0.1.0", + "from": "forwarded@~0.1.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz" + }, + "ipaddr.js": { + "version": "1.0.5", + "from": "ipaddr.js@1.0.5", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz" + } + } + }, + "qs": { + "version": "2.4.2", + "from": "qs@2.4.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz" + }, + "range-parser": { + "version": "1.0.3", + "from": "range-parser@~1.0.2", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz" + }, + "send": { + "version": "0.13.0", + "from": "send@0.13.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz", + "dependencies": { + "destroy": { + "version": "1.0.3", + "from": "destroy@1.0.3", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz" + }, + "http-errors": { + "version": "1.3.1", + "from": "http-errors@~1.3.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "mime": { + "version": "1.3.4", + "from": "mime@1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "statuses": { + "version": "1.2.1", + "from": "statuses@~1.2.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" + } + } + }, + "serve-static": { + "version": "1.10.3", + "from": "serve-static@~1.10.0", + "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", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + }, + "send": { + "version": "0.13.2", + "from": "send@0.13.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", + "dependencies": { + "depd": { + "version": "1.1.0", + "from": "depd@~1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" + }, + "destroy": { + "version": "1.0.4", + "from": "destroy@~1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + }, + "http-errors": { + "version": "1.3.1", + "from": "http-errors@~1.3.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "mime": { + "version": "1.3.4", + "from": "mime@1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "statuses": { + "version": "1.2.1", + "from": "statuses@~1.2.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz" + } + } + } + } + }, + "type-is": { + "version": "1.6.13", + "from": "type-is@~1.6.3", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + } + } + } + } + }, + "vary": { + "version": "1.0.1", + "from": "vary@~1.0.0", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz" + }, + "utils-merge": { + "version": "1.0.0", + "from": "utils-merge@1.0.0", + "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", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.11.3.tgz", + "dependencies": { + "cookie": { + "version": "0.1.3", + "from": "cookie@0.1.3", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz" + }, + "cookie-signature": { + "version": "1.0.6", + "from": "cookie-signature@1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + }, + "crc": { + "version": "3.3.0", + "from": "crc@3.3.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "depd": { + "version": "1.0.1", + "from": "depd@~1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz" + }, + "on-headers": { + "version": "1.0.1", + "from": "on-headers@~1.0.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz" + }, + "parseurl": { + "version": "1.3.1", + "from": "parseurl@~1.3.0", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" + }, + "uid-safe": { + "version": "2.0.0", + "from": "uid-safe@~2.0.0", + "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", + "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", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" + } + } + }, + "grunt": { + "version": "0.4.5", + "from": "grunt@^0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "dependencies": { + "async": { + "version": "0.1.22", + "from": "async@~0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz" + }, + "coffee-script": { + "version": "1.3.3", + "from": "coffee-script@~1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz" + }, + "colors": { + "version": "0.6.2", + "from": "colors@~0.6.2", + "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", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz" + }, + "eventemitter2": { + "version": "0.4.14", + "from": "eventemitter2@~0.4.13", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" + }, + "findup-sync": { + "version": "0.1.3", + "from": "findup-sync@~0.1.2", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "dependencies": { + "glob": { + "version": "3.2.11", + "from": "glob@~3.2.9", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "minimatch": { + "version": "0.3.0", + "from": "minimatch@0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + } + } + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + } + } + }, + "glob": { + "version": "3.1.21", + "from": "glob@~3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "dependencies": { + "graceful-fs": { + "version": "1.2.3", + "from": "graceful-fs@~1.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" + }, + "inherits": { + "version": "1.0.2", + "from": "inherits@1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" + } + } + }, + "hooker": { + "version": "0.2.3", + "from": "hooker@~0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" + }, + "iconv-lite": { + "version": "0.2.11", + "from": "iconv-lite@~0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz" + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@~0.2.12", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + }, + "nopt": { + "version": "1.0.10", + "from": "nopt@~1.0.10", + "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" + } + } + }, + "rimraf": { + "version": "2.2.8", + "from": "rimraf@~2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" + }, + "lodash": { + "version": "0.9.2", + "from": "lodash@~0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz" + }, + "underscore.string": { + "version": "2.2.1", + "from": "underscore.string@~2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz" + }, + "which": { + "version": "1.0.9", + "from": "which@~1.0.5", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz" + }, + "js-yaml": { + "version": "2.0.5", + "from": "js-yaml@~2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "dependencies": { + "argparse": { + "version": "0.1.16", + "from": "argparse@~ 0.1.11", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "dependencies": { + "underscore": { + "version": "1.7.0", + "from": "underscore@~1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz" + }, + "underscore.string": { + "version": "2.4.0", + "from": "underscore.string@~2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz" + } + } + }, + "esprima": { + "version": "1.0.4", + "from": "esprima@~ 1.0.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" + } + } + }, + "exit": { + "version": "0.1.2", + "from": "exit@~0.1.1", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + }, + "getobject": { + "version": "0.1.0", + "from": "getobject@~0.1.0", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz" + }, + "underscore.string": { + "version": "2.3.3", + "from": "underscore.string@~2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" + } + } + } + } + }, + "heapdump": { + "version": "0.3.7", + "from": "heapdump@^0.3.7", + "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", + "dependencies": { + "eventemitter3": { + "version": "1.2.0", + "from": "eventemitter3@1.x.x", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz" + }, + "requires-port": { + "version": "1.0.0", + "from": "requires-port@1.x.x", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + } + } + }, + "jade": { + "version": "1.3.1", + "from": "jade@~1.3.1", + "resolved": "https://registry.npmjs.org/jade/-/jade-1.3.1.tgz", + "dependencies": { + "commander": { + "version": "2.1.0", + "from": "commander@2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz" + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@~0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + }, + "transformers": { + "version": "2.1.0", + "from": "transformers@2.1.0", + "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", + "dependencies": { + "promise": { + "version": "2.0.0", + "from": "promise@~2.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "dependencies": { + "is-promise": { + "version": "1.0.1", + "from": "is-promise@~1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz" + } + } + }, + "css": { + "version": "1.0.8", + "from": "css@~1.0.8", + "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", + "dependencies": { + "css-parse": { + "version": "1.0.4", + "from": "css-parse@1.0.4", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + }, + "optimist": { + "version": "0.3.7", + "from": "optimist@~0.3.5", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@~0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + } + } + } + } + } + } + }, + "character-parser": { + "version": "1.2.0", + "from": "character-parser@1.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.0.tgz" + }, + "monocle": { + "version": "1.1.51", + "from": "monocle@1.1.51", + "resolved": "https://registry.npmjs.org/monocle/-/monocle-1.1.51.tgz", + "dependencies": { + "readdirp": { + "version": "0.2.5", + "from": "readdirp@~0.2.3", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + } + } + } + } + }, + "with": { + "version": "3.0.1", + "from": "with@~3.0.0", + "resolved": "https://registry.npmjs.org/with/-/with-3.0.1.tgz", + "dependencies": { + "uglify-js": { + "version": "2.4.24", + "from": "uglify-js@~2.4.12", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "dependencies": { + "async": { + "version": "0.2.10", + "from": "async@~0.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" + }, + "source-map": { + "version": "0.1.34", + "from": "source-map@0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "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", + "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", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "dependencies": { + "camelcase": { + "version": "1.2.1", + "from": "camelcase@^1.0.2", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + }, + "decamelize": { + "version": "1.2.0", + "from": "decamelize@^1.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + }, + "window-size": { + "version": "0.1.0", + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" + }, + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + } + } + } + } + } + } + }, + "constantinople": { + "version": "2.0.1", + "from": "constantinople@~2.0.0", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-2.0.1.tgz", + "dependencies": { + "uglify-js": { + "version": "2.4.24", + "from": "uglify-js@~2.4.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz", + "dependencies": { + "async": { + "version": "0.2.10", + "from": "async@~0.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" + }, + "source-map": { + "version": "0.1.34", + "from": "source-map@0.1.34", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz", + "dependencies": { + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "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", + "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", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz", + "dependencies": { + "camelcase": { + "version": "1.2.1", + "from": "camelcase@^1.0.2", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + }, + "decamelize": { + "version": "1.2.0", + "from": "decamelize@^1.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + }, + "window-size": { + "version": "0.1.0", + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" + }, + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + } + } + } + } + } + } + } + } + }, + "ldapjs": { + "version": "1.0.0", + "from": "ldapjs@^1.0.0", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.0.tgz", + "dependencies": { + "asn1": { + "version": "0.2.3", + "from": "asn1@0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "0.1.5", + "from": "assert-plus@0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" + }, + "bunyan": { + "version": "1.5.1", + "from": "bunyan@1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "dependencies": { + "mv": { + "version": "2.1.1", + "from": "mv@~2", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@~0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "ncp": { + "version": "2.0.0", + "from": "ncp@~2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" + }, + "rimraf": { + "version": "2.4.5", + "from": "rimraf@~2.4.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "dependencies": { + "glob": { + "version": "6.0.4", + "from": "glob@^6.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "dependencies": { + "inflight": { + "version": "1.0.5", + "from": "inflight@^1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@^1.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + } + } + }, + "safe-json-stringify": { + "version": "1.0.3", + "from": "safe-json-stringify@~1", + "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", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz" + }, + "backoff": { + "version": "2.4.1", + "from": "backoff@2.4.1", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.4.1.tgz", + "dependencies": { + "precond": { + "version": "0.2.3", + "from": "precond@0.2", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz" + } + } + }, + "ldap-filter": { + "version": "0.2.2", + "from": "ldap-filter@0.2.2", + "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz" + }, + "once": { + "version": "1.3.2", + "from": "once@1.3.2", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.2.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "vasync": { + "version": "1.6.3", + "from": "vasync@1.6.3", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.3.tgz" + }, + "verror": { + "version": "1.6.0", + "from": "verror@1.6.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz", + "dependencies": { + "extsprintf": { + "version": "1.2.0", + "from": "extsprintf@1.2.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz" + } + } + }, + "dtrace-provider": { + "version": "0.6.0", + "from": "dtrace-provider@~0.6", + "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" + } + } + } + } + }, + "lodash": { + "version": "4.13.1", + "from": "lodash@^4.13.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz" + }, + "logger-sharelatex": { + "version": "1.3.1", + "from": "logger-sharelatex@git+https://github.com/sharelatex/logger-sharelatex.git#v1.3.1", + "resolved": "git+https://github.com/sharelatex/logger-sharelatex.git#bf413ec621a000cf0e08c939de38d5e24541a08c", + "dependencies": { + "bunyan": { + "version": "1.5.1", + "from": "bunyan@1.5.1", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.5.1.tgz", + "dependencies": { + "dtrace-provider": { + "version": "0.6.0", + "from": "dtrace-provider@~0.6", + "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" + } + } + }, + "mv": { + "version": "2.1.1", + "from": "mv@~2", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@~0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "ncp": { + "version": "2.0.0", + "from": "ncp@~2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" + }, + "rimraf": { + "version": "2.4.5", + "from": "rimraf@~2.4.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "dependencies": { + "glob": { + "version": "6.0.4", + "from": "glob@^6.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "dependencies": { + "inflight": { + "version": "1.0.5", + "from": "inflight@^1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "once": { + "version": "1.3.3", + "from": "once@^1.3.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "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", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + } + } + }, + "safe-json-stringify": { + "version": "1.0.3", + "from": "safe-json-stringify@~1", + "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", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.4.0.tgz" + }, + "raven": { + "version": "0.8.1", + "from": "raven@^0.8.0", + "resolved": "https://registry.npmjs.org/raven/-/raven-0.8.1.tgz", + "dependencies": { + "cookie": { + "version": "0.1.0", + "from": "cookie@0.1.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" + }, + "lsmod": { + "version": "0.0.3", + "from": "lsmod@~0.0.3", + "resolved": "https://registry.npmjs.org/lsmod/-/lsmod-0.0.3.tgz" + }, + "stack-trace": { + "version": "0.0.7", + "from": "stack-trace@0.0.7", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.7.tgz" + } + } + } + } + }, + "lynx": { + "version": "0.1.1", + "from": "lynx@0.1.1", + "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz", + "dependencies": { + "mersenne": { + "version": "0.0.3", + "from": "mersenne@~0.0.3", + "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.3.tgz" + }, + "statsd-parser": { + "version": "0.0.4", + "from": "statsd-parser@~0.0.4", + "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" + }, + "method-override": { + "version": "2.3.6", + "from": "method-override@^2.3.3", + "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.6.tgz", + "dependencies": { + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "methods": { + "version": "1.1.2", + "from": "methods@~1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + }, + "parseurl": { + "version": "1.3.1", + "from": "parseurl@~1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" + }, + "vary": { + "version": "1.1.0", + "from": "vary@~1.1.0", + "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", + "resolved": "git+https://github.com/sharelatex/metrics-sharelatex.git#080c4aeb696edcd5d6d86f202f2c528f0661d7a6", + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "from": "coffee-script@1.6.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.0.tgz" + } + } + }, + "mimelib": { + "version": "0.2.14", + "from": "mimelib@0.2.14", + "resolved": "https://registry.npmjs.org/mimelib/-/mimelib-0.2.14.tgz", + "dependencies": { + "encoding": { + "version": "0.1.12", + "from": "encoding@~0.1", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "dependencies": { + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@~0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + } + } + }, + "addressparser": { + "version": "0.2.1", + "from": "addressparser@~0.2.0", + "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-0.2.1.tgz" + } + } + }, + "mocha": { + "version": "1.17.1", + "from": "mocha@1.17.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.17.1.tgz", + "dependencies": { + "commander": { + "version": "2.0.0", + "from": "commander@2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" + }, + "growl": { + "version": "1.7.0", + "from": "growl@1.7.x", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz" + }, + "jade": { + "version": "0.26.3", + "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "dependencies": { + "commander": { + "version": "0.6.1", + "from": "commander@0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + }, + "mkdirp": { + "version": "0.3.0", + "from": "mkdirp@0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + } + } + }, + "diff": { + "version": "1.0.7", + "from": "diff@1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@*", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + }, + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "dependencies": { + "minimatch": { + "version": "0.2.14", + "from": "minimatch@~0.2.11", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@~2.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "mongojs": { + "version": "0.18.2", + "from": "mongojs@0.18.2", + "resolved": "https://registry.npmjs.org/mongojs/-/mongojs-0.18.2.tgz", + "dependencies": { + "thunky": { + "version": "0.1.0", + "from": "thunky@~0.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@1.1.x", + "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", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "mongodb": { + "version": "1.4.32", + "from": "mongodb@1.4.32", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.4.32.tgz", + "dependencies": { + "bson": { + "version": "0.2.22", + "from": "bson@~0.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.2.22.tgz", + "dependencies": { + "nan": { + "version": "1.8.4", + "from": "nan@~1.8", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.8.4.tgz" + } + } + }, + "kerberos": { + "version": "0.0.9", + "from": "kerberos@0.0.9", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.9.tgz", + "dependencies": { + "nan": { + "version": "1.6.2", + "from": "nan@1.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.6.2.tgz" + } + } + }, + "readable-stream": { + "version": "2.1.4", + "from": "readable-stream@latest", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + } + } + } + }, + "mongoose": { + "version": "4.1.0", + "from": "mongoose@4.1.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.1.0.tgz", + "dependencies": { + "async": { + "version": "0.9.0", + "from": "async@0.9.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz" + }, + "bson": { + "version": "0.3.2", + "from": "bson@~0.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.3.2.tgz", + "dependencies": { + "bson-ext": { + "version": "0.1.13", + "from": "bson-ext@~0.1", + "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-0.1.13.tgz", + "dependencies": { + "bindings": { + "version": "1.2.1", + "from": "bindings@^1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz" + }, + "nan": { + "version": "2.0.9", + "from": "nan@~2.0.9", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.0.9.tgz" + } + } + } + } + }, + "hooks-fixed": { + "version": "1.1.0", + "from": "hooks-fixed@1.1.0", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-1.1.0.tgz" + }, + "kareem": { + "version": "1.0.1", + "from": "kareem@1.0.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.0.1.tgz" + }, + "mongodb": { + "version": "2.0.34", + "from": "mongodb@2.0.34", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.0.34.tgz", + "dependencies": { + "mongodb-core": { + "version": "1.2.0", + "from": "mongodb-core@1.2.0", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-1.2.0.tgz", + "dependencies": { + "bson": { + "version": "0.4.23", + "from": "bson@~0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-0.4.23.tgz" + }, + "kerberos": { + "version": "0.0.21", + "from": "kerberos@~0.0", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.21.tgz", + "dependencies": { + "nan": { + "version": "2.3.5", + "from": "nan@~2.3", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.3.5.tgz" + } + } + } + } + }, + "readable-stream": { + "version": "1.0.31", + "from": "readable-stream@1.0.31", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz", + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@~1.0.0", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "mpath": { + "version": "0.1.1", + "from": "mpath@0.1.1", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.1.1.tgz" + }, + "mpromise": { + "version": "0.5.4", + "from": "mpromise@0.5.4", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.4.tgz" + }, + "mquery": { + "version": "1.6.1", + "from": "mquery@1.6.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-1.6.1.tgz", + "dependencies": { + "bluebird": { + "version": "2.9.26", + "from": "bluebird@2.9.26", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.9.26.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + } + } + }, + "ms": { + "version": "0.1.0", + "from": "ms@0.1.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.1.0.tgz" + }, + "muri": { + "version": "1.0.0", + "from": "muri@1.0.0", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.0.0.tgz" + }, + "regexp-clone": { + "version": "0.0.1", + "from": "regexp-clone@0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz" + }, + "sliced": { + "version": "0.0.5", + "from": "sliced@0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz" + } + } + }, + "multer": { + "version": "0.1.8", + "from": "multer@^0.1.8", + "resolved": "https://registry.npmjs.org/multer/-/multer-0.1.8.tgz", + "dependencies": { + "busboy": { + "version": "0.2.13", + "from": "busboy@~0.2.9", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.13.tgz", + "dependencies": { + "dicer": { + "version": "0.2.5", + "from": "dicer@0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "dependencies": { + "streamsearch": { + "version": "0.1.2", + "from": "streamsearch@0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz" + } + } + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@1.1.x", + "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", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@~0.10.x", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@~0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + }, + "qs": { + "version": "1.2.2", + "from": "qs@~1.2.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-1.2.2.tgz" + }, + "type-is": { + "version": "1.5.7", + "from": "type-is@~1.5.2", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz" + } + } + } + } + } + } + }, + "node-uuid": { + "version": "1.4.1", + "from": "node-uuid@1.4.1", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.1.tgz" + }, + "nodemailer": { + "version": "2.1.0", + "from": "nodemailer@2.1.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.1.0.tgz", + "dependencies": { + "libmime": { + "version": "2.0.0", + "from": "libmime@2.0.0", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-2.0.0.tgz", + "dependencies": { + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + }, + "libbase64": { + "version": "0.1.0", + "from": "libbase64@0.1.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz" + }, + "libqp": { + "version": "1.1.0", + "from": "libqp@1.1.0", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz" + } + } + }, + "mailcomposer": { + "version": "3.3.2", + "from": "mailcomposer@3.3.2", + "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-3.3.2.tgz", + "dependencies": { + "buildmail": { + "version": "3.3.2", + "from": "buildmail@3.3.2", + "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-3.3.2.tgz", + "dependencies": { + "addressparser": { + "version": "1.0.0", + "from": "addressparser@1.0.0", + "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.0.tgz" + }, + "libbase64": { + "version": "0.1.0", + "from": "libbase64@0.1.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz" + }, + "libqp": { + "version": "1.1.0", + "from": "libqp@1.1.0", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz" + }, + "nodemailer-fetch": { + "version": "1.2.1", + "from": "nodemailer-fetch@1.2.1", + "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.2.1.tgz" + } + } + } + } + }, + "nodemailer-direct-transport": { + "version": "2.0.1", + "from": "nodemailer-direct-transport@2.0.1", + "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", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" + }, + "nodemailer-wellknown": { + "version": "0.1.7", + "from": "nodemailer-wellknown@0.1.7", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" + }, + "nodemailer-wellknown": { + "version": "0.1.7", + "from": "nodemailer-wellknown@0.1.7", + "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", + "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.0.1.tgz" + } + } + } + } + }, + "nodemailer-sendgrid-transport": { + "version": "0.2.0", + "from": "nodemailer-sendgrid-transport@^0.2.0", + "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", + "resolved": "https://registry.npmjs.org/sendgrid/-/sendgrid-1.9.2.tgz", + "dependencies": { + "mime": { + "version": "1.3.4", + "from": "mime@^1.2.9", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz" + }, + "lodash": { + "version": "3.10.1", + "from": "lodash@^3.0.1 || ^2.0.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz" + }, + "smtpapi": { + "version": "1.2.0", + "from": "smtpapi@^1.2.0", + "resolved": "https://registry.npmjs.org/smtpapi/-/smtpapi-1.2.0.tgz" + } + } + } + } + }, + "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", + "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", + "dependencies": { + "sax": { + "version": "1.1.5", + "from": "sax@1.1.5", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.5.tgz" + }, + "xml2js": { + "version": "0.4.15", + "from": "xml2js@0.4.15", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.15.tgz" + }, + "xmlbuilder": { + "version": "2.6.2", + "from": "xmlbuilder@2.6.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.6.2.tgz", + "dependencies": { + "lodash": { + "version": "3.5.0", + "from": "lodash@~3.5.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz" + } + } + }, + "jmespath": { + "version": "0.15.0", + "from": "jmespath@0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz" + } + } + } + } + }, + "optimist": { + "version": "0.6.1", + "from": "optimist@0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@~0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + }, + "minimist": { + "version": "0.0.10", + "from": "minimist@~0.0.1", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + } + } + }, + "redback": { + "version": "0.4.0", + "from": "redback@0.4.0", + "resolved": "https://registry.npmjs.org/redback/-/redback-0.4.0.tgz" + }, + "redis": { + "version": "0.10.1", + "from": "redis@0.10.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-0.10.1.tgz" + }, + "redis-sharelatex": { + "version": "0.0.9", + "from": "redis-sharelatex@0.0.9", + "resolved": "https://registry.npmjs.org/redis-sharelatex/-/redis-sharelatex-0.0.9.tgz", + "dependencies": { + "chai": { + "version": "1.9.1", + "from": "chai@1.9.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-1.9.1.tgz", + "dependencies": { + "assertion-error": { + "version": "1.0.0", + "from": "assertion-error@1.0.0", + "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", + "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", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" + } + } + } + } + }, + "coffee-script": { + "version": "1.8.0", + "from": "coffee-script@1.8.0", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.8.0.tgz", + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@~0.3.5", + "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", + "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", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.7.1.tgz", + "dependencies": { + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@~0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + } + } + }, + "chalk": { + "version": "0.5.1", + "from": "chalk@~0.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "dependencies": { + "ansi-styles": { + "version": "1.1.0", + "from": "ansi-styles@^1.1.0", + "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", + "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", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + } + } + }, + "lodash": { + "version": "2.4.2", + "from": "lodash@~2.4.1", + "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", + "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", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz" + }, + "fs-extra": { + "version": "0.11.1", + "from": "fs-extra@~0.11.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.11.1.tgz", + "dependencies": { + "ncp": { + "version": "0.6.0", + "from": "ncp@^0.6.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-0.6.0.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@^0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "jsonfile": { + "version": "2.3.1", + "from": "jsonfile@^2.0.0", + "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", + "dependencies": { + "glob": { + "version": "7.0.3", + "from": "glob@^7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "dependencies": { + "inflight": { + "version": "1.0.5", + "from": "inflight@^1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "once": { + "version": "1.3.3", + "from": "once@^1.3.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "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", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + } + } + } + } + }, + "mocha": { + "version": "1.21.4", + "from": "mocha@1.21.4", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.21.4.tgz", + "dependencies": { + "commander": { + "version": "2.0.0", + "from": "commander@2.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz" + }, + "growl": { + "version": "1.8.1", + "from": "growl@1.8.x", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz" + }, + "jade": { + "version": "0.26.3", + "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "dependencies": { + "commander": { + "version": "0.6.1", + "from": "commander@0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + }, + "mkdirp": { + "version": "0.3.0", + "from": "mkdirp@0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + } + } + }, + "diff": { + "version": "1.0.7", + "from": "diff@1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@*", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + } + } + }, + "mkdirp": { + "version": "0.3.5", + "from": "mkdirp@0.3.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" + }, + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "dependencies": { + "minimatch": { + "version": "0.2.14", + "from": "minimatch@~0.2.11", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "dependencies": { + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@~1.0.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + } + } + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@~2.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "redis": { + "version": "0.12.1", + "from": "redis@0.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-0.12.1.tgz" + }, + "redis-sentinel": { + "version": "0.1.1", + "from": "redis-sentinel@0.1.1", + "resolved": "https://registry.npmjs.org/redis-sentinel/-/redis-sentinel-0.1.1.tgz", + "dependencies": { + "redis": { + "version": "0.11.0", + "from": "redis@0.11.x", + "resolved": "https://registry.npmjs.org/redis/-/redis-0.11.0.tgz" + }, + "q": { + "version": "0.9.2", + "from": "q@0.9.2", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.2.tgz" + } + } + }, + "sandboxed-module": { + "version": "1.0.1", + "from": "sandboxed-module@1.0.1", + "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", + "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", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz" + } + } + }, + "sinon": { + "version": "1.10.3", + "from": "sinon@1.10.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.10.3.tgz", + "dependencies": { + "formatio": { + "version": "1.0.2", + "from": "formatio@~1.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.0.2.tgz", + "dependencies": { + "samsam": { + "version": "1.1.3", + "from": "samsam@~1.1", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.3.tgz" + } + } + }, + "util": { + "version": "0.10.3", + "from": "util@>=0.10.3 <1", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + } + } + }, + "underscore": { + "version": "1.7.0", + "from": "underscore@1.7.0", + "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", + "dependencies": { + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@~0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "aws4": { + "version": "1.4.1", + "from": "aws4@^1.2.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz" + }, + "bl": { + "version": "1.1.2", + "from": "bl@~1.1.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@~2.0.5", + "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", + "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", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + } + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@~0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@~1.0.5", + "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", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + } + } + }, + "extend": { + "version": "3.0.0", + "from": "extend@~3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@~0.6.1", + "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", + "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", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + } + } + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@~2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "dependencies": { + "chalk": { + "version": "1.1.3", + "from": "chalk@^1.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@^2.2.1", + "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", + "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", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } + } + }, + "commander": { + "version": "2.9.0", + "from": "commander@^2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>= 1.0.0", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + } + } + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@^4.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@^2.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "dependencies": { + "pinkie": { + "version": "2.0.4", + "from": "pinkie@^2.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + } + } + } + } + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@~3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "dependencies": { + "hoek": { + "version": "2.16.3", + "from": "hoek@2.x.x", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@2.x.x", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@2.x.x", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@1.x.x", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + } + } + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@~1.1.0", + "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", + "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", + "dependencies": { + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "json-schema": { + "version": "0.2.2", + "from": "json-schema@0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "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", + "dependencies": { + "asn1": { + "version": "0.2.3", + "from": "asn1@~0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@^1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + }, + "dashdash": { + "version": "1.14.0", + "from": "dashdash@^1.12.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz" + }, + "getpass": { + "version": "0.1.6", + "from": "getpass@^0.1.1", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz" + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@~0.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" + }, + "tweetnacl": { + "version": "0.13.3", + "from": "tweetnacl@~0.13.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@^1.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@~0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" + } + } + } + } + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@~1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@~0.1.2", + "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", + "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", + "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", + "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", + "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", + "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" + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@~0.0.4", + "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" + }, + "tunnel-agent": { + "version": "0.4.3", + "from": "tunnel-agent@~0.4.1", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" + } + } + }, + "requests": { + "version": "0.1.7", + "from": "requests@^0.1.7", + "resolved": "https://registry.npmjs.org/requests/-/requests-0.1.7.tgz", + "dependencies": { + "axo": { + "version": "0.0.1", + "from": "axo@0.0.x", + "resolved": "https://registry.npmjs.org/axo/-/axo-0.0.1.tgz" + }, + "eventemitter3": { + "version": "1.1.1", + "from": "eventemitter3@1.1.x", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.1.1.tgz" + }, + "extendible": { + "version": "0.1.1", + "from": "extendible@0.1.x", + "resolved": "https://registry.npmjs.org/extendible/-/extendible-0.1.1.tgz" + }, + "hang": { + "version": "1.0.0", + "from": "hang@1.0.x", + "resolved": "https://registry.npmjs.org/hang/-/hang-1.0.0.tgz" + }, + "loads": { + "version": "0.0.4", + "from": "loads@0.0.x", + "resolved": "https://registry.npmjs.org/loads/-/loads-0.0.4.tgz", + "dependencies": { + "failure": { + "version": "1.1.1", + "from": "failure@1.1.x", + "resolved": "https://registry.npmjs.org/failure/-/failure-1.1.1.tgz" + }, + "one-time": { + "version": "0.0.4", + "from": "one-time@0.0.x", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/xhr-send/-/xhr-send-1.0.0.tgz" + } + } + }, + "rimraf": { + "version": "2.2.6", + "from": "rimraf@2.2.6", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz" + }, + "sanitizer": { + "version": "0.1.1", + "from": "sanitizer@0.1.1", + "resolved": "https://registry.npmjs.org/sanitizer/-/sanitizer-0.1.1.tgz" + }, + "settings-sharelatex": { + "version": "1.0.0", + "from": "settings-sharelatex@git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", + "resolved": "git+https://github.com/sharelatex/settings-sharelatex.git#cbc5e41c1dbe6789721a14b3fdae05bf22546559", + "dependencies": { + "coffee-script": { + "version": "1.6.0", + "from": "coffee-script@1.6.0", + "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", + "resolved": "https://registry.npmjs.org/sixpack-client/-/sixpack-client-1.0.0.tgz" + }, + "temp": { + "version": "0.8.3", + "from": "temp@^0.8.3", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", + "dependencies": { + "os-tmpdir": { + "version": "1.0.1", + "from": "os-tmpdir@^1.0.0", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz" + } + } + }, + "underscore": { + "version": "1.6.0", + "from": "underscore@1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" + }, + "v8-profiler": { + "version": "5.6.5", + "from": "v8-profiler@^5.2.3", + "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" + }, + "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", + "dependencies": { + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@~0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "nopt": { + "version": "3.0.6", + "from": "nopt@~3.0.1", + "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" + } + } + }, + "npmlog": { + "version": "2.0.4", + "from": "npmlog@~2.0.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.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", + "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", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + } + }, + "gauge": { + "version": "1.2.7", + "from": "gauge@~1.2.5", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", + "dependencies": { + "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" + }, + "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", + "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" + } + } + }, + "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", + "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" + } + } + }, + "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" + } + } + } + } + } + } + }, + "rc": { + "version": "1.1.6", + "from": "rc@~1.1.0", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", + "dependencies": { + "deep-extend": { + "version": "0.4.1", + "from": "deep-extend@~0.4.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz" + }, + "ini": { + "version": "1.3.4", + "from": "ini@~1.3.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz" + }, + "minimist": { + "version": "1.2.0", + "from": "minimist@^1.2.0", + "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", + "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", + "dependencies": { + "glob": { + "version": "7.0.3", + "from": "glob@^7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "dependencies": { + "inflight": { + "version": "1.0.5", + "from": "inflight@^1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + }, + "once": { + "version": "1.3.3", + "from": "once@^1.3.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "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", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + } + } + } + } + }, + "semver": { + "version": "5.1.0", + "from": "semver@~5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" + }, + "tar": { + "version": "2.2.1", + "from": "tar@~2.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "dependencies": { + "block-stream": { + "version": "0.0.9", + "from": "block-stream@*", + "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", + "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" + } + } + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.1", + "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", + "dependencies": { + "debug": { + "version": "2.2.0", + "from": "debug@~2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "dependencies": { + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "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", + "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" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@~2.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + } + } + }, + "fstream-ignore": { + "version": "1.0.5", + "from": "fstream-ignore@~1.0.3", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "dependencies": { + "inherits": { + "version": "2.0.1", + "from": "inherits@2", + "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", + "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", + "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" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + } + } + } + } + } + } + }, + "once": { + "version": "1.3.3", + "from": "once@~1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "dependencies": { + "wrappy": { + "version": "1.0.2", + "from": "wrappy@1", + "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", + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@~1.0.0", + "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", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@~1.0.0", + "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", + "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", + "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", + "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", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + } + } + } + } + } + } + }, + "xml2js": { + "version": "0.2.0", + "from": "xml2js@0.2.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.0.tgz", + "dependencies": { + "sax": { + "version": "1.2.1", + "from": "sax@>=0.1.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" + } + } + } + } +} diff --git a/services/web/package.json b/services/web/package.json index 754182a632..0c0c4bb436 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -18,6 +18,7 @@ "body-parser": "^1.13.1", "bufferedstream": "1.6.0", "connect-redis": "2.3.0", + "contentful": "^3.3.14", "cookie": "^0.2.3", "cookie-parser": "1.3.5", "csurf": "^1.8.3", @@ -30,8 +31,9 @@ "jade": "~1.3.1", "ldapjs": "^0.7.1", "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", + "lodash": "^4.13.1", "lynx": "0.1.1", - "marked": "^0.3.3", + "marked": "^0.3.5", "method-override": "^2.3.3", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0", "mimelib": "0.2.14", @@ -54,32 +56,35 @@ "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0", "sixpack-client": "^1.0.0", "temp": "^0.8.3", - "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master", "underscore": "1.6.0", - "underscore.string": "^3.0.2", "v8-profiler": "^5.2.3", "xml2js": "0.2.0" }, "devDependencies": { "bunyan": "0.22.1", + "translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master", "chai": "", "chai-spies": "", "grunt": "0.4.1", "grunt-available-tasks": "0.4.1", "grunt-bunyan": "0.5.0", - "grunt-concurrent": "0.4.3", "grunt-contrib-clean": "0.5.0", "grunt-contrib-coffee": "0.10.0", "grunt-contrib-less": "0.9.0", "grunt-contrib-requirejs": "0.4.1", - "grunt-execute": "0.1.5", + "grunt-contrib-watch": "^1.0.0", + "grunt-env": "0.4.4", + "clean-css": "^3.4.18", + "grunt-exec": "^0.4.7", + "grunt-execute": "^0.2.2", "grunt-file-append": "0.0.6", "grunt-git-rev-parse": "^0.1.4", "grunt-mocha-test": "0.9.0", + "grunt-newer": "^1.2.0", + "grunt-parallel": "^0.5.1", "grunt-sed": "^0.1.1", "sandboxed-module": "0.2.0", "sinon": "", - "timekeeper": "", - "grunt-env": "0.4.4" + "timekeeper": "" } } diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index ceebadac46..4c75ab5d77 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -42,7 +42,7 @@ define [ ReferencesManager ) -> - App.controller "IdeController", ($scope, $timeout, ide, localStorage) -> + App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) -> # Don't freak out if we're already in an apply callback $scope.$originalApply = $scope.$apply $scope.$apply = (fn = () ->) -> @@ -69,6 +69,17 @@ define [ $scope.chat = {} + # Tracking code. + $scope.$watch "ui.view", (newView, oldView) -> + if newView? and newView != "editor" and newView != "pdf" + event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once" + + $scope.$watch "ui.chatOpen", (isOpen) -> + event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen + + $scope.$watch "ui.leftMenuShown", (isOpen) -> + event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen + # End of tracking code. window._ide = ide diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 596f350812..800d6bd194 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -74,15 +74,17 @@ define [ editor.commands.removeCommand "foldall" # For European keyboards, the / is above 7 so needs Shift pressing. - # This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment. + # This comes through as Command-Shift-/ on OS X, which is mapped to + # toggleBlockComment. # This doesn't do anything for LaTeX, so remap this to togglecomment to # work for European keyboards as normal. + # On Windows, the key combo comes as Ctrl-Shift-7. editor.commands.removeCommand "toggleBlockComment" editor.commands.removeCommand "togglecomment" editor.commands.addCommand { name: "togglecomment", - bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" }, + bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" }, exec: (editor) -> editor.toggleCommentLines(), multiSelectAction: "forEachLine", scrollIntoView: "selectionPart" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee index 75e00a37f6..c5b37d1f3b 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee @@ -134,6 +134,8 @@ define [ editor.completer.showPopup(editor) editor.completer.cancelContextMenu() $(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'}) + if editor.completer?.completions?.filtered?.length == 0 + editor.completer.detach() bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space" } diff --git a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee index 4d0adef4fa..36eaa5d555 100644 --- a/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee +++ b/services/web/public/coffee/ide/hotkeys/controllers/HotkeysController.coffee @@ -2,8 +2,10 @@ define [ "base" "ace/ace" ], (App) -> - App.controller "HotkeysController", ($scope, $modal) -> + App.controller "HotkeysController", ($scope, $modal, event_tracking) -> $scope.openHotkeysModal = -> + event_tracking.sendCountly "ide-open-hotkeys-modal" + $modal.open { templateUrl: "hotkeysModalTemplate" controller: "HotkeysModalController" diff --git a/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee new file mode 100644 index 0000000000..747ca1ad08 --- /dev/null +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogs.coffee @@ -0,0 +1,20 @@ +define [ + "libs/latex-log-parser" + "ide/human-readable-logs/HumanReadableLogsRules" +], (LogParser, ruleset) -> + parse : (rawLog, options) -> + parsedLogEntries = LogParser.parse(rawLog, options) + + _getRule = (logMessage) -> + return rule for rule in ruleset when rule.regexToMatch.test logMessage + + 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? + + entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint? + entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL? + + 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 new file mode 100644 index 0000000000..f123515deb --- /dev/null +++ b/services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee @@ -0,0 +1,91 @@ +define -> [ + regexToMatch: /Misplaced alignment tab character \&/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_alignment_tab_character_%26" + humanReadableHint: """ + You have placed an alignment tab character '&' in the wrong place. If you want to align something, you must write it inside an align environment such as \\begin{align} \u2026 \\end{align}, \\begin{tabular} \u2026 \\end{tabular}, etc. If you want to write an ampersand '&' in text, you must write \\& instead. + """ + , + regexToMatch: /Extra alignment tab has been changed to \\cr/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr" + humanReadableHint: """ + You have written too many alignment tabs in a table, causing one of them to be turned into a line break. Make sure you have specified the correct number of columns in your table. + """ + , + regexToMatch: /Display math should end with \$\$/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$" + humanReadableHint: """ + You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ \u2026 $$. Check that the number of $s match around each math expression. + """ + , + 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}. + """ + , + regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references" + humanReadableHint: """ + You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}. + """ + , + regexToMatch: /Citation .+ on page .+ undefined on input line .+/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX" + humanReadableHint: """ + You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way. + """ + , + regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels" + humanReadableHint: """ + You have used the same label more than once. Check that each \\label{...} labels only one item. + """ + , + regexToMatch: /`!?h' float specifier changed to `!?ht'/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27" + humanReadableHint: """ + The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the float package. + """ + , + regexToMatch: /No positions in optional float specifier/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier" + humanReadableHint: """ + You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (e.g. \begin{figure}[h]), or remove the square brackets (e.g. \begin{figure}). Find out more about float specifiers here. + """ + , + regexToMatch: /Undefined control sequence/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence" + humanReadableHint: """ + The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}. + """ + , + regexToMatch: /File .+ not found/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX" + humanReadableHint: """ + The compiler cannot find the file you want to include. Make sure that you have uploaded the file and specified the file location correctly. + """ + , + regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX" + humanReadableHint: """ + The compiler does not recognise the file type of one of your images. Make sure you are using a supported image format for your choice of compiler, and check that there are no periods (.) in the name of your image. + """ + , + regexToMatch: /LaTeX Error: Unknown float option `H'/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27" + humanReadableHint: """ + The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this. + """ + , + regexToMatch: /LaTeX Error: Unknown float option `q'/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27" + humanReadableHint: """ + You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures here. + """ + , + regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/ + extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode" + 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. + """ +] diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 05d99124c7..0f8134258c 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -1,15 +1,67 @@ define [ "base" - "libs/latex-log-parser" + "ace/ace" + "ide/human-readable-logs/HumanReadableLogs" "libs/bib-log-parser" -], (App, LogParser, BibLogParser) -> - App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) -> + "services/log-hints-feedback" +], (App, Ace, HumanReadableLogs, BibLogParser) -> + App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) -> + # enable per-user containers by default + perUserCompile = true autoCompile = true # pdf.view = uncompiled | pdf | errors $scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled' $scope.shouldShowLogs = false + $scope.wikiEnabled = window.wikiEnabled; + + # view logic to check whether the files dropdown should "drop up" or "drop down" + $scope.shouldDropUp = false + + logsContainerEl = document.querySelector ".pdf-logs" + filesDropdownEl = logsContainerEl?.querySelector ".files-dropdown" + + # get the top coordinate of the files dropdown as a ratio (to the logs container height) + # logs container supports scrollable content, so it's possible that ratio > 1. + getFilesDropdownTopCoordAsRatio = () -> + filesDropdownEl?.getBoundingClientRect().top / logsContainerEl?.getBoundingClientRect().height + + $scope.$watch "shouldShowLogs", (shouldShow) -> + if shouldShow + $scope.$applyAsync () -> + $scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65 + + # log hints tracking + $scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts + + $scope.trackLogHintsLearnMore = () -> + event_tracking.sendCountly "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 } + + $scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) -> + logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal + + $scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId + $scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId + + if ace.require("ace/lib/useragent").isMac + $scope.modifierKey = "Cmd" + else + $scope.modifierKey = "Ctrl" + + # utility for making a query string from a hash, could use jquery $.param + createQueryString = (args) -> + qs_args = ("#{k}=#{v}" for k, v of args) + if qs_args.length then "?" + qs_args.join("&") else "" + + $scope.stripHTMLFromString = (htmlStr) -> + tmp = document.createElement("DIV") + tmp.innerHTML = htmlStr + return tmp.textContent || tmp.innerText || "" $scope.$on "project:joined", () -> return if !autoCompile @@ -28,13 +80,14 @@ define [ sendCompileRequest = (options = {}) -> url = "/project/#{$scope.project_id}/compile" + params = {} if options.isAutoCompile - url += "?auto_compile=true" + params["auto_compile"]=true return $http.post url, { rootDoc_id: options.rootDocOverride_id or null draft: $scope.draft _csrf: window.csrfToken - } + }, {params: params} parseCompileResponse = (response) -> @@ -42,20 +95,36 @@ define [ $scope.pdf.error = false $scope.pdf.timedout = false $scope.pdf.failure = false - $scope.pdf.projectTooLarge = false $scope.pdf.url = null $scope.pdf.clsiMaintenance = false $scope.pdf.tooRecentlyCompiled = false $scope.pdf.renderingError = false + $scope.pdf.projectTooLarge = false + $scope.pdf.compileTerminated = false # make a cache to look up files by name fileByPath = {} - for file in response.outputFiles - fileByPath[file.path] = file + if response?.outputFiles? + for file in response?.outputFiles + fileByPath[file.path] = file + + # prepare query string + qs = {} + # add a query string parameter for the compile group + if response.compileGroup? + ide.compileGroup = qs.compileGroup = response.compileGroup + # add a query string parameter for the clsi server id + if response.clsiServerId? + ide.clsiServerId = qs.clsiserverid = response.clsiServerId if response.status == "timedout" $scope.pdf.view = 'errors' $scope.pdf.timedout = true + fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) + else if response.status == "terminated" + $scope.pdf.view = 'errors' + $scope.pdf.compileTerminated = true + fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) else if response.status == "autocompile-backoff" $scope.pdf.view = 'uncompiled' else if response.status == "project-too-large" @@ -72,12 +141,13 @@ define [ else if response.status == "too-recently-compiled" $scope.pdf.view = 'errors' $scope.pdf.tooRecentlyCompiled = true + else if response.status == "validation-problems" + $scope.pdf.view = "validation-problems" + $scope.pdf.validation = response.validationProblems else if response.status == "success" $scope.pdf.view = 'pdf' $scope.shouldShowLogs = false - # prepare query string - qs = {} # define the base url. if the pdf file has a build number, pass it to the clsi in the url if fileByPath['output.pdf']?.url? $scope.pdf.url = fileByPath['output.pdf'].url @@ -89,17 +159,11 @@ define [ # check if we need to bust cache (build id is unique so don't need it in that case) if not fileByPath['output.pdf']?.build? qs.cache_bust = "#{Date.now()}" - # add a query string parameter for the compile group - if response.compileGroup? - $scope.pdf.compileGroup = response.compileGroup - qs.compileGroup = "#{$scope.pdf.compileGroup}" - if response.clsiServerId? - qs.clsiserverid = response.clsiServerId - ide.clsiServerId = response.clsiServerId # convert the qs hash into a query string and append it - qs_args = ("#{k}=#{v}" for k, v of qs) - $scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else "" - $scope.pdf.url += $scope.pdf.qs + $scope.pdf.url += createQueryString qs + # Save all downloads as files + qs.popupDownload = true + $scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs) fetchLogs(fileByPath['output.log'], fileByPath['output.blg']) @@ -108,17 +172,18 @@ define [ if !response.outputFiles? return + + # prepare list of output files for download dropdown + qs = {} + if response.clsiServerId? + qs.clsiserverid = response.clsiServerId for file in response.outputFiles if IGNORE_FILES.indexOf(file.path) == -1 - # Turn 'output.blg' into 'blg file'. - if file.path.match(/^output\./) - file.name = "#{file.path.replace(/^output\./, "")} file" - else - file.name = file.path - file.url = "/project/#{project_id}/output/#{file.path}" - if response.clsiServerId? - file.url = file.url + "?clsiserverid=#{response.clsiServerId}" - $scope.pdf.outputFiles.push file + $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 + url: "/project/#{project_id}/output/#{file.path}" + createQueryString qs + } fetchLogs = (logFile, blgFile) -> @@ -127,14 +192,17 @@ define [ opts = method:"GET" params: - build:file.build + compileGroup:ide.compileGroup clsiserverid:ide.clsiServerId - if file.url? # FIXME clean this up when we have file.urls out consistently + if file?.url? # FIXME clean this up when we have file.urls out consistently opts.url = file.url else if file?.build? opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}" else opts.url = "/project/#{$scope.project_id}/output/#{name}" + # check if we need to bust cache (build id is unique so don't need it in that case) + if not file?.build? + opts.params.cache_bust = "#{Date.now()}" return $http(opts) # accumulate the log entries @@ -150,7 +218,7 @@ define [ # use the parsers for each file type processLog = (log) -> $scope.pdf.rawLog = log - {errors, warnings, typesetting} = LogParser.parse(log, ignoreDuplicates: true) + {errors, warnings, typesetting} = HumanReadableLogs.parse(log, ignoreDuplicates: true) all = [].concat errors, warnings, typesetting accumulateResults {all, errors, warnings} @@ -204,7 +272,7 @@ define [ return null normalizeFilePath = (path) -> - path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "") + path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "") path = path.replace(/^\/compile\//, "") rootDocDirname = ide.fileTreeManager.getRootDocDirname() @@ -215,6 +283,9 @@ define [ $scope.recompile = (options = {}) -> return if $scope.pdf.compiling + + event_tracking.sendCountlySampled "editor-recompile-sampled", options + $scope.pdf.compiling = true ide.$scope.$broadcast("flush-changes") @@ -234,6 +305,21 @@ define [ # This needs to be public. ide.$scope.recompile = $scope.recompile + # This method is a simply wrapper and exists only for tracking purposes. + ide.$scope.recompileViaKey = () -> + $scope.recompile { keyShortcut: true } + + $scope.stop = () -> + return if !$scope.pdf.compiling + + $http { + url: "/project/#{$scope.project_id}/compile/stop" + method: "POST" + params: + clsiserverid:ide.clsiServerId + headers: + "X-Csrf-Token": window.csrfToken + } $scope.clearCache = () -> $http { @@ -247,6 +333,7 @@ define [ $scope.toggleLogs = () -> $scope.shouldShowLogs = !$scope.shouldShowLogs + event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs $scope.showPdf = () -> $scope.pdf.view = "pdf" @@ -254,6 +341,7 @@ define [ $scope.toggleRawLog = () -> $scope.pdf.showRawLog = !$scope.pdf.showRawLog + event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog $scope.openClearCacheModal = () -> modalInstance = $modal.open( @@ -287,10 +375,16 @@ define [ $scope.startFreeTrial = (source) -> ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source) + + event_tracking.sendCountly "subscription-start-trial", { source } + window.open("/user/subscription/new?planCode=student_free_trial_7_days") $scope.startedFreeTrial = true App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) -> + # enable per-user containers by default + perUserCompile = true + synctex = syncToPdf: (cursorPosition) -> deferred = $q.defer() @@ -339,18 +433,35 @@ define [ deferred.reject() return deferred.promise + # FIXME: this actually works better if it's halfway across the + # page (or the visible part of the page). Synctex doesn't + # always find the right place in the file when the point is at + # the edge of the page, it sometimes returns the start of the + # next paragraph instead. + h = position.offset.left + + # Compute the vertical position to pass to synctex, which + # works with coordinates increasing from the top of the page + # down. This matches the browser's DOM coordinate of the + # click point, but the pdf position is measured from the + # bottom of the page so we need to invert it. + if options.fromPdfPosition and position.pageSize?.height? + v = (position.pageSize.height - position.offset.top) or 0 # measure from pdf point (inverted) + else + v = position.offset.top or 0 # measure from html click position + # It's not clear exactly where we should sync to if it wasn't directly # clicked on, but a little bit down from the very top seems best. if options.includeVisualOffset - position.offset.top = position.offset.top + 80 + v += 72 # use the same value as in pdfViewer highlighting visual offset $http({ url: "/project/#{ide.project_id}/sync/pdf", method: "GET", params: { page: position.page + 1 - h: position.offset.left.toFixed(2) - v: position.offset.top.toFixed(2) + h: h.toFixed(2) + v: v.toFixed(2) clsiserverid:ide.clsiServerId } }) @@ -380,14 +491,15 @@ define [ $scope.syncToCode = () -> synctex - .syncToCode($scope.pdf.position, includeVisualOffset: true) + .syncToCode($scope.pdf.position, includeVisualOffset: true, fromPdfPosition: true) .then (data) -> {doc, line} = data ide.editorManager.openDoc(doc, gotoLine: line) ] - App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) -> + App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) -> $scope.openInEditor = (entry) -> + event_tracking.sendCountlyOnce "logs-jump-to-location-once" entity = ide.fileTreeManager.findEntityByPath(entry.file) return if !entity? or entity.type != "doc" if entry.line? diff --git a/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee index 2df89bc35f..f1c161ffc6 100644 --- a/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee +++ b/services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee @@ -1,23 +1,14 @@ define [ "base" "ide/pdfng/directives/pdfViewer" - "text!libs/pdfListView/TextLayer.css" - "text!libs/pdfListView/AnnotationsLayer.css" - "text!libs/pdfListView/HighlightsLayer.css" ], ( App pdfViewer - textLayerCss - annotationsLayerCss - highlightsLayerCss + ) -> if PDFJS? PDFJS.workerSrc = window.pdfJsWorkerPath - style = $("