Merge branch 'master' into node-4.2

This commit is contained in:
Henry Oswald
2016-10-19 13:04:26 +01:00
546 changed files with 333784 additions and 40343 deletions
+10 -12
View File
@@ -47,23 +47,21 @@ TpdsWorker.js
BackgroundJobsWorker.js
UserAndProjectPopulator.coffee
public/js/history/versiondetail.js
!public/js/libs/
public/js/*
!public/js/ace/*
!public/js/libs/
public/js/*.js
public/js/libs/sharejs.js
public/js/editor.js
public/js/home.js
public/js/forms.js
public/js/gui.js
public/js/admin.js
public/js/history/*
public/js/analytics/
public/js/directives/
public/js/filters/
public/js/ide/
public/js/main/
public/js/modules/
public/js/services/
public/js/utils/
public/stylesheets/style.css
public/brand/plans.css
public/minjs/
public/js/main.js
Gemfile.lock
*.swp
+12 -61
View File
@@ -1,4 +1,5 @@
fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-coffee'
@@ -157,12 +158,13 @@ module.exports = (grunt) ->
inlineText: false
preserveLicenseComments: false
paths:
"moment": "libs/moment-2.9.0"
"moment": "libs/#{PackageVersions.lib('moment')}"
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML"
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
"pdfjs-dist/build/pdf": "libs/#{PackageVersions.lib('pdfjs')}/pdf"
"ace": "#{PackageVersions.lib('ace')}"
shim:
"libs/pdf":
deps: ["libs/pdfjs-1.3.91/compatibility"]
"pdfjs-dist/build/pdf":
deps: ["libs/#{PackageVersions.lib('pdfjs')}/compatibility"]
skipDirOptimize: true
modules: [
@@ -171,11 +173,13 @@ module.exports = (grunt) ->
exclude: ["libs"]
}, {
name: "ide",
exclude: ["libs", "libs/pdf"]
exclude: ["libs", "pdfjs-dist/build/pdf"]
}, {
name: "libs"
},{
name: "ace/mode-latex"
},{
name: "ace/worker-latex"
}
]
@@ -199,7 +203,7 @@ module.exports = (grunt) ->
acceptance:
src: ["test/acceptance/js/#{grunt.option('feature') or '**'}/*.js"]
options:
timeout: 10000
timeout: 40000
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
@@ -380,63 +384,10 @@ module.exports = (grunt) ->
grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
grunt.registerTask 'run:watch', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'exec']
grunt.registerTask 'default', 'run'
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
done = @async()
email = grunt.option("email")
if !email?
console.error "Usage: grunt create-admin-user --email joe@example.com"
process.exit(1)
settings = require "settings-sharelatex"
UserRegistrationHandler = require "./app/js/Features/User/UserRegistrationHandler"
OneTimeTokenHandler = require "./app/js/Features/Security/OneTimeTokenHandler"
UserRegistrationHandler.registerNewUser {
email: email
password: require("crypto").randomBytes(32).toString("hex")
}, (error, user) ->
if error? and error?.message != "EmailAlreadyRegistered"
throw error
user.isAdmin = true
user.save (error) ->
throw error if error?
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
return next(err) if err?
console.log ""
console.log """
Successfully created #{email} as an admin user.
Please visit the following URL to set a password for #{email} and log in:
#{settings.siteUrl}/user/password/set?passwordResetToken=#{token}
"""
done()
grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
done = @async()
email = grunt.option("email")
if !email?
console.error "Usage: grunt delete-user --email joe@example.com"
process.exit(1)
settings = require "settings-sharelatex"
UserGetter = require "./app/js/Features/User/UserGetter"
UserDeleter = require "./app/js/Features/User/UserDeleter"
UserGetter.getUser email:email, (error, user) ->
if error?
throw error
if !user?
console.log("user #{email} not in database, potentially already deleted")
return done()
UserDeleter.deleteUser user._id, (err)->
if err?
throw err
done()
@@ -0,0 +1,7 @@
AnalyticsManager = require "./AnalyticsManager"
module.exports = AnalyticsController =
recordEvent: (req, res, next) ->
AnalyticsManager.recordEvent req.session?.user?._id, req.params.event, req.body, (error) ->
return next(error) if error?
res.send 204
@@ -0,0 +1,43 @@
Settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
_ = require "underscore"
if !Settings.analytics?.postgres?
module.exports =
recordEvent: (user_id, event, segmentation, callback = () ->) ->
logger.log {user_id, event, segmentation}, "no event tracking configured, logging event"
callback()
else
Sequelize = require "sequelize"
options = _.extend {logging:false}, Settings.analytics.postgres
sequelize = new Sequelize(
Settings.analytics.postgres.database,
Settings.analytics.postgres.username,
Settings.analytics.postgres.password,
options
)
Event = sequelize.define("Event", {
user_id: Sequelize.STRING,
event: Sequelize.STRING,
segmentation: Sequelize.JSONB
})
module.exports =
recordEvent: (user_id, event, segmentation = {}, callback = (error) ->) ->
if user_id? and typeof(user_id) != "string"
user_id = user_id.toString()
if user_id == Settings.smokeTest?.userId
# Don't record smoke tests analytics
return callback()
Event
.create({ user_id, event, segmentation })
.then(
(result) -> callback(),
(error) ->
logger.err {err: error, user_id, event, segmentation}, "error recording analytics event"
callback(error)
)
sync: () -> sequelize.sync()
@@ -10,62 +10,115 @@ Settings = require "settings-sharelatex"
basicAuth = require('basic-auth-connect')
UserHandler = require("../User/UserHandler")
UserSessionsManager = require("../User/UserSessionsManager")
Analytics = require "../Analytics/AnalyticsManager"
passport = require 'passport'
module.exports = AuthenticationController =
login: (req, res, next = (error) ->) ->
AuthenticationController.doLogin req.body, req, res, next
doLogin: (options, req, res, next) ->
email = options.email?.toLowerCase()
password = options.password
redir = Url.parse(options.redir or "/project").path
serializeUser: (user, callback) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
email: user.email
referal_id: user.referal_id
session_created: (new Date()).toISOString()
ip_address: user._login_req_ip
callback(null, lightUser)
deserializeUser: (user, cb) ->
cb(null, user)
passportLogin: (req, res, next) ->
# This function is middleware which wraps the passport.authenticate middleware,
# so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
# and send a `{redir: ""}` response on success
passport.authenticate('local', (err, user, info) ->
if err?
return next(err)
if user # `user` is either a user object or false
req.login user, (err) ->
# Regenerate the session to get a new sessionID (cookie value) to
# protect against session fixation attacks
oldSession = req.session
req.session.destroy()
req.sessionStore.generate(req)
for key, value of oldSession
req.session[key] = value
# copy to the old `session.user` location, for backward-comptability
req.session.user = req.session.passport.user
req.session.save (err) ->
if err?
logger.err {user_id: user._id}, "error saving regenerated session after login"
return next(err)
UserSessionsManager.trackSession(user, req.sessionID, () ->)
res.json {redir: req._redir}
else
res.json message: info
)(req, res, next)
doPassportLogin: (req, username, password, done) ->
email = username.toLowerCase()
redir = Url.parse(req?.body?.redir or "/project").path
LoginRateLimiter.processLoginRequest email, (err, isAllowed)->
return done(err) if err?
if !isAllowed
logger.log email:email, "too many login requests"
res.statusCode = 429
return res.send
message:
text: req.i18n.translate("to_many_login_requests_2_mins"),
type: 'error'
return done(null, null, {text: req.i18n.translate("to_many_login_requests_2_mins"), type: 'error'})
AuthenticationManager.authenticate email: email, password, (error, user) ->
return next(error) if error?
return done(error) if error?
if user?
UserHandler.setupLoginData user, ->
LoginRateLimiter.recordSuccessfulLogin email
AuthenticationController._recordSuccessfulLogin user._id
AuthenticationController.establishUserSession req, user, (error) ->
return next(error) if error?
req.session.justLoggedIn = true
logger.log email: email, user_id: user._id.toString(), "successful log in"
res.json redir: redir
# async actions
UserHandler.setupLoginData(user, ()->)
LoginRateLimiter.recordSuccessfulLogin(email)
AuthenticationController._recordSuccessfulLogin(user._id)
Analytics.recordEvent(user._id, "user-logged-in")
logger.log email: email, user_id: user._id.toString(), "successful log in"
req.session.justLoggedIn = true
# capture the request ip for use when creating the session
user._login_req_ip = req.ip
req._redir = redir
return done(null, user)
else
AuthenticationController._recordFailedLogin()
logger.log email: email, "failed log in"
res.json message:
text: req.i18n.translate("email_or_password_wrong_try_again"),
type: 'error'
return done(null, false, {text: req.i18n.translate("email_or_password_wrong_try_again"), type: 'error'})
getLoggedInUserId: (req, callback = (error, user_id) ->) ->
if req?.session?.user?._id?
callback null, req.session.user._id.toString()
setInSessionUser: (req, props) ->
for key, value of props
if req?.session?.passport?.user?
req.session.passport.user[key] = value
if req?.session?.user?
req.session.user[key] = value
isUserLoggedIn: (req) ->
user_id = AuthenticationController.getLoggedInUserId(req)
return (user_id not in [null, undefined, false])
# TODO: perhaps should produce an error if the current user is not present
getLoggedInUserId: (req) ->
user = AuthenticationController.getSessionUser(req)
if user
return user._id
else
callback null, null
return null
getLoggedInUser: (req, callback = (error, user) ->) ->
if req.session?.user?._id?
query = req.session.user._id
getSessionUser: (req) ->
if req?.session?.user?
return req.session.user
else if req?.session?.passport?.user
return req.session.passport.user
else
return callback null, null
UserGetter.getUser query, callback
return null
requireLogin: () ->
doRequest = (req, res, next = (error) ->) ->
if !req.session.user?
if !AuthenticationController.isUserLoggedIn(req)
AuthenticationController._redirectToLoginOrRegisterPage(req, res)
else
req.user = req.session.user
return next()
req.user = AuthenticationController.getSessionUser(req)
next()
return doRequest
@@ -79,7 +132,7 @@ module.exports = AuthenticationController =
if req.headers['authorization']?
return AuthenticationController.httpAuth(req, res, next)
else if req.session.user?
else if AuthenticationController.isUserLoggedIn(req)
return next()
else
logger.log url:req.url, "user trying to access endpoint not in global whitelist"
@@ -97,7 +150,6 @@ module.exports = AuthenticationController =
else
AuthenticationController._redirectToLoginPage(req, res)
_redirectToLoginPage: (req, res) ->
logger.log url: req.url, "user not logged in so redirecting to login page"
req.query.redir = req.path
@@ -124,26 +176,3 @@ module.exports = AuthenticationController =
_recordFailedLogin: (callback = (error) ->) ->
Metrics.inc "user.login.failed"
callback()
establishUserSession: (req, user, callback = (error) ->) ->
lightUser =
_id: user._id
first_name: user.first_name
last_name: user.last_name
isAdmin: user.isAdmin
email: user.email
referal_id: user.referal_id
session_created: (new Date()).toISOString()
ip_address: req.ip
# Regenerate the session to get a new sessionID (cookie value) to
# protect against session fixation attacks
oldSession = req.session
req.session.destroy()
req.sessionStore.generate(req)
for key, value of oldSession
req.session[key] = value
req.session.user = lightUser
UserSessionsManager.trackSession(user, req.sessionID, () ->)
callback()
@@ -29,6 +29,9 @@ module.exports = AuthenticationManager =
callback null, null
setUserPassword: (user_id, password, callback = (error) ->) ->
if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length
return callback("password is too long")
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
return callback(error) if error?
bcrypt.hash password, salt, (error, hash) ->
@@ -3,6 +3,7 @@ async = require "async"
logger = require "logger-sharelatex"
ObjectId = require("mongojs").ObjectId
Errors = require "../Errors/Errors"
AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = AuthorizationMiddlewear =
ensureUserCanReadMultipleProjects: (req, res, next) ->
@@ -20,7 +21,7 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear.redirectToRestricted req, res, next
else
next()
ensureUserCanReadProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -32,7 +33,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user read access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanWriteProjectSettings: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -44,7 +45,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanWriteProjectContent: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -56,7 +57,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user write access to project settings"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserCanAdminProject: (req, res, next) ->
AuthorizationMiddlewear._getUserAndProjectId req, (error, user_id, project_id) ->
return next(error) if error?
@@ -68,7 +69,7 @@ module.exports = AuthorizationMiddlewear =
else
logger.log {user_id, project_id}, "denying user admin access to project"
AuthorizationMiddlewear.redirectToRestricted req, res, next
ensureUserIsSiteAdmin: (req, res, next) ->
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return next(error) if error?
@@ -90,22 +91,22 @@ module.exports = AuthorizationMiddlewear =
AuthorizationMiddlewear._getUserId req, (error, user_id) ->
return callback(error) if error?
callback(null, user_id, project_id)
_getUserId: (req, callback = (error, user_id) ->) ->
if req.session?.user?._id?
user_id = req.session.user._id
else
user_id = null
callback null, user_id
user_id = AuthenticationController.getLoggedInUserId(req)
return callback(null, user_id)
redirectToRestricted: (req, res, next) ->
res.redirect "/restricted"
res.redirect "/restricted?from=#{encodeURIComponent(req.url)}"
restricted : (req, res, next)->
if req.session.user?
if AuthenticationController.isUserLoggedIn(req)
res.render 'user/restricted',
title:'restricted'
else
logger.log "user not logged in and trying to access #{req.url}, being redirected to login"
res.redirect '/register'
from = req.query.from
logger.log {from: from}, "redirecting to login"
redirect_to = "/login"
if from?
redirect_to += "?redir=#{encodeURIComponent(from)}"
res.redirect redirect_to
@@ -2,14 +2,15 @@ BetaProgramHandler = require './BetaProgramHandler'
UserLocator = require "../User/UserLocator"
Settings = require "settings-sharelatex"
logger = require 'logger-sharelatex'
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = BetaProgramController =
optIn: (req, res, next) ->
user_id = req?.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting in to beta program"
if !user_id
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optIn user_id, (err) ->
if err
@@ -17,9 +18,9 @@ module.exports = BetaProgramController =
return res.redirect "/beta/participate"
optOut: (req, res, next) ->
user_id = req?.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "user opting out of beta program"
if !user_id
if !user_id?
return next(new Error("no user id in session"))
BetaProgramHandler.optOut user_id, (err) ->
if err
@@ -27,7 +28,7 @@ module.exports = BetaProgramController =
return res.redirect "/beta/participate"
optInPage: (req, res, next)->
user_id = req.session?.user?._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "showing beta participation page for user"
UserLocator.findById user_id, (err, user)->
if err
@@ -28,7 +28,7 @@ module.exports = BlogController =
try
data = JSON.parse(data)
if settings.cdn?.web?.host?
data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
data?.content = data?.content?.replace(/src="(\/[^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
catch err
logger.err err:err, data:data, "error parsing data from data"
res.render "blog/blog_holder", data
@@ -1,14 +1,18 @@
ChatHandler = require("./ChatHandler")
EditorRealTimeController = require("../Editor/EditorRealTimeController")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
sendMessage: (req, res)->
sendMessage: (req, res, next)->
project_id = req.params.Project_id
user_id = req.session.user._id
messageContent = req.body.content
user_id = AuthenticationController.getLoggedInUserId(req)
if !user_id?
err = new Error('no logged-in user')
return next(err)
ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)->
if err?
logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api"
@@ -4,7 +4,9 @@ ProjectEditorHandler = require "../Project/ProjectEditorHandler"
EditorRealTimeController = require "../Editor/EditorRealTimeController"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
mimelib = require("mimelib")
EmailHelper = require "../Helpers/EmailHelper"
logger = require 'logger-sharelatex'
module.exports = CollaboratorsController =
addUserToProject: (req, res, next) ->
@@ -16,11 +18,11 @@ module.exports = CollaboratorsController =
return res.json { user: false }
else
{email, privileges} = req.body
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
email = EmailHelper.parseEmail(email)
if !email? or email == ""
return res.status(400).send("invalid email address")
adding_user_id = req.session?.user?._id
CollaboratorsHandler.addEmailToProject project_id, adding_user_id, email, privileges, (error, user_id) =>
return next(error) if error?
@@ -35,8 +37,9 @@ module.exports = CollaboratorsController =
user_id = req.params.user_id
CollaboratorsController._removeUserIdFromProject project_id, user_id, (error) ->
return next(error) if error?
EditorRealTimeController.emitToRoom project_id, 'project:membership:changed', {members: true}
res.sendStatus 204
removeSelfFromProject: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
user_id = req.session?.user?._id
@@ -50,3 +53,11 @@ module.exports = CollaboratorsController =
EditorRealTimeController.emitToRoom(project_id, 'userRemovedFromProject', user_id)
callback()
getAllMembers: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active members for project"
CollaboratorsHandler.getAllMembers projectId, (err, members) ->
if err?
logger.err {projectId}, "error getting members for project"
return next(err)
res.json({members: members})
@@ -2,7 +2,15 @@ Project = require("../../models/Project").Project
EmailHandler = require("../Email/EmailHandler")
Settings = require "settings-sharelatex"
module.exports =
module.exports = CollaboratorsEmailHandler =
_buildInviteUrl: (project, invite) ->
"#{Settings.siteUrl}/project/#{project._id}/invite/token/#{invite.token}?" + [
"project_name=#{encodeURIComponent(project.name)}"
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
notifyUserOfProjectShare: (project_id, email, callback)->
Project
.findOne(_id: project_id )
@@ -22,4 +30,19 @@ module.exports =
"rs=ci" # referral source = collaborator invite
].join("&")
owner: project.owner_ref
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
Project
.findOne(_id: project_id )
.select("name owner_ref")
.populate('owner_ref')
.exec (err, project)->
emailOptions =
to: email
replyTo: project.owner_ref.email
project:
name: project.name
inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite)
owner: project.owner_ref
EmailHandler.sendEmail "projectInvite", emailOptions, callback
@@ -1,6 +1,5 @@
UserCreator = require('../User/UserCreator')
Project = require("../../models/Project").Project
mimelib = require("mimelib")
logger = require('logger-sharelatex')
UserGetter = require "../User/UserGetter"
ContactManager = require "../Contacts/ContactManager"
@@ -8,8 +7,12 @@ CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
EmailHelper = require "../Helpers/EmailHelper"
ProjectEditorHandler = require "../Project/ProjectEditorHandler"
module.exports = CollaboratorsHandler =
getMemberIdsWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
Project.findOne { _id: project_id }, { owner_ref: 1, collaberator_refs: 1, readOnly_refs: 1 }, (error, project) ->
return callback(error) if error?
@@ -21,12 +24,12 @@ module.exports = CollaboratorsHandler =
for member_id in project.collaberator_refs or []
members.push { id: member_id.toString(), privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
return callback null, members
getMemberIds: (project_id, callback = (error, member_ids) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, members.map (m) -> m.id
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
@@ -41,7 +44,7 @@ module.exports = CollaboratorsHandler =
(error) ->
return callback(error) if error?
callback null, result
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
# then optimise this.
@@ -51,12 +54,12 @@ module.exports = CollaboratorsHandler =
if member.id == user_id?.toString()
return callback null, member.privilegeLevel
return callback null, PrivilegeLevels.NONE
getMemberCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members) ->
return callback(error) if error?
return callback null, (members or []).length
getCollaboratorCount: (project_id, callback = (error, count) ->) ->
CollaboratorsHandler.getMemberCount project_id, (error, count) ->
return callback(error) if error?
@@ -69,14 +72,14 @@ module.exports = CollaboratorsHandler =
if member.id.toString() == user_id.toString()
return callback null, true, member.privilegeLevel
return callback null, false, null
getProjectsUserIsCollaboratorOf: (user_id, fields, callback = (error, readAndWriteProjects, readOnlyProjects) ->) ->
Project.find {collaberator_refs:user_id}, fields, (err, readAndWriteProjects)=>
return callback(err) if err?
Project.find {readOnly_refs:user_id}, fields, (err, readOnlyProjects)=>
return callback(err) if err?
callback(null, readAndWriteProjects, readOnlyProjects)
removeUserFromProject: (project_id, user_id, callback = (error) ->)->
logger.log user_id: user_id, project_id: project_id, "removing user"
conditions = _id:project_id
@@ -86,7 +89,7 @@ module.exports = CollaboratorsHandler =
if err?
logger.error err: err, "problem removing user from project collaberators"
callback(err)
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
return callback(error) if error?
@@ -98,10 +101,9 @@ module.exports = CollaboratorsHandler =
return cb() if !project?
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
async.series jobs, callback
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
emails = mimelib.parseAddresses(unparsed_email)
email = emails[0]?.address?.toLowerCase()
email = EmailHelper.parseEmail(unparsed_email)
if !email? or email == ""
return callback(new Error("no valid email provided: '#{unparsed_email}'"))
UserCreator.getUserOrCreateHoldingAccount email, (error, user) ->
@@ -118,7 +120,7 @@ module.exports = CollaboratorsHandler =
existing_users = existing_users.map (u) -> u.toString()
if existing_users.indexOf(user_id.toString()) > -1
return callback null # User already in Project
if privilegeLevel == PrivilegeLevels.READ_AND_WRITE
level = {"collaberator_refs":user_id}
logger.log {privileges: "readAndWrite", user_id, project_id}, "adding user"
@@ -128,11 +130,6 @@ module.exports = CollaboratorsHandler =
else
return callback(new Error("unknown privilegeLevel: #{privilegeLevel}"))
# Do these in the background
UserGetter.getUser user_id, {email: 1}, (error, user) ->
if error?
logger.error {err: error, project_id, user_id}, "error getting user while adding to project"
CollaboratorsEmailHandler.notifyUserOfProjectShare project_id, user.email
ContactManager.addContact adding_user_id, user_id
Project.update { _id: project_id }, { $addToSet: level }, (error) ->
@@ -143,3 +140,12 @@ module.exports = CollaboratorsHandler =
if error?
logger.error {err: error, project_id, user_id}, "error flushing to TPDS after adding collaborator"
callback()
getAllMembers: (projectId, callback=(err, members)->) ->
logger.log {projectId}, "fetching all members"
CollaboratorsHandler.getMembersWithPrivilegeLevels projectId, (error, rawMembers) ->
if error?
logger.err {projectId, error}, "error getting members for project"
return callback(error)
{owner, members} = ProjectEditorHandler.buildOwnerAndMembersViews(rawMembers)
callback(null, members)
@@ -0,0 +1,127 @@
ProjectGetter = require "../Project/ProjectGetter"
LimitationsManager = require "../Subscription/LimitationsManager"
UserGetter = require "../User/UserGetter"
CollaboratorsHandler = require('./CollaboratorsHandler')
CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler')
logger = require('logger-sharelatex')
EmailHelper = require "../Helpers/EmailHelper"
EditorRealTimeController = require("../Editor/EditorRealTimeController")
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
AnalyticsManger = require("../Analytics/AnalyticsManager")
AuthenticationController = require("../Authentication/AuthenticationController")
module.exports = CollaboratorsInviteController =
getAllInvites: (req, res, next) ->
projectId = req.params.Project_id
logger.log {projectId}, "getting all active invites for project"
CollaboratorsInviteHandler.getAllInvites projectId, (err, invites) ->
if err?
logger.err {projectId}, "error getting invites for project"
return next(err)
res.json({invites: invites})
inviteToProject: (req, res, next) ->
projectId = req.params.Project_id
email = req.body.email
sendingUser = AuthenticationController.getSessionUser(req)
sendingUserId = sendingUser._id
logger.log {projectId, email, sendingUserId}, "inviting to project"
LimitationsManager.canAddXCollaborators projectId, 1, (error, allowed) =>
return next(error) if error?
if !allowed
logger.log {projectId, email, sendingUserId}, "not allowed to invite more users to project"
return res.json {invite: null}
{email, privileges} = req.body
email = EmailHelper.parseEmail(email)
if !email? or email == ""
logger.log {projectId, email, sendingUserId}, "invalid email address"
return res.sendStatus(400)
CollaboratorsInviteHandler.inviteToProject projectId, sendingUser, email, privileges, (err, invite) ->
if err?
logger.err {projectId, email, sendingUserId}, "error creating project invite"
return next(err)
logger.log {projectId, email, sendingUserId}, "invite created"
EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', {invites: true})
return res.json {invite: invite}
revokeInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "revoking invite"
CollaboratorsInviteHandler.revokeInvite projectId, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error revoking invite"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true}
res.sendStatus(201)
resendInvite: (req, res, next) ->
projectId = req.params.Project_id
inviteId = req.params.invite_id
logger.log {projectId, inviteId}, "resending invite"
sendingUser = AuthenticationController.getSessionUser(req)
CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite"
return next(err)
res.sendStatus(201)
viewInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
_renderInvalidPage = () ->
logger.log {projectId, token}, "invite not valid, rendering not-valid page"
res.render "project/invite/not-valid", {title: "Invalid Invite"}
# check if the user is already a member of the project
currentUser = AuthenticationController.getSessionUser(req)
CollaboratorsHandler.isUserMemberOfProject currentUser._id, projectId, (err, isMember, _privilegeLevel) ->
if err?
logger.err {err, projectId}, "error checking if user is member of project"
return next(err)
if isMember
logger.log {projectId, userId: currentUser._id}, "user is already a member of this project, redirecting"
return res.redirect "/project/#{projectId}"
# get the invite
CollaboratorsInviteHandler.getInviteByToken projectId, token, (err, invite) ->
if err?
logger.err {projectId, token}, "error getting invite by token"
return next(err)
# check if invite is gone, or otherwise non-existent
if !invite?
logger.log {projectId, token}, "no invite found for this token"
return _renderInvalidPage()
# check the user who sent the invite exists
UserGetter.getUser {_id: invite.sendingUserId}, {email: 1, first_name: 1, last_name: 1}, (err, owner) ->
if err?
logger.err {err, projectId}, "error getting project owner"
return next(err)
if !owner?
logger.log {projectId}, "no project owner found"
return _renderInvalidPage()
# fetch the project name
ProjectGetter.getProject projectId, {}, (err, project) ->
if err?
logger.err {err, projectId}, "error getting project"
return next(err)
if !project?
logger.log {projectId}, "no project found"
return _renderInvalidPage()
# finally render the invite
res.render "project/invite/show", {invite, project, owner, title: "Project Invite"}
acceptInvite: (req, res, next) ->
projectId = req.params.Project_id
token = req.params.token
currentUser = AuthenticationController.getSessionUser(req)
logger.log {projectId, userId: currentUser._id, token}, "got request to accept invite"
CollaboratorsInviteHandler.acceptInvite projectId, token, currentUser, (err) ->
if err?
logger.err {projectId, token}, "error accepting invite by token"
return next(err)
EditorRealTimeController.emitToRoom projectId, 'project:membership:changed', {invites: true, members: true}
AnalyticsManger.recordEvent(currentUser._id, "project-invite-accept", {projectId:projectId, userId:currentUser._id})
if req.xhr
res.sendStatus 204 # Done async via project page notification
else
res.redirect "/project/#{projectId}"
@@ -0,0 +1,144 @@
ProjectInvite = require("../../models/ProjectInvite").ProjectInvite
logger = require('logger-sharelatex')
CollaboratorsEmailHandler = require "./CollaboratorsEmailHandler"
CollaboratorsHandler = require "./CollaboratorsHandler"
UserGetter = require "../User/UserGetter"
ProjectGetter = require "../Project/ProjectGetter"
Async = require "async"
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
Errors = require "../Errors/Errors"
Crypto = require 'crypto'
NotificationsBuilder = require("../Notifications/NotificationsBuilder")
module.exports = CollaboratorsInviteHandler =
getAllInvites: (projectId, callback=(err, invites)->) ->
logger.log {projectId}, "fetching invites for project"
ProjectInvite.find {projectId: projectId}, (err, invites) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
logger.log {projectId, count: invites.length}, "found invites for project"
callback(null, invites)
getInviteCount: (projectId, callback=(err, count)->) ->
logger.log {projectId}, "counting invites for project"
ProjectInvite.count {projectId: projectId}, (err, count) ->
if err?
logger.err {err, projectId}, "error getting invites from mongo"
return callback(err)
callback(null, count)
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
email = invite.email
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
if err?
logger.err {projectId, email}, "error checking if user exists"
return callback(err)
if !existingUser?
logger.log {projectId, email}, "no existing user found, returning"
return callback(null)
ProjectGetter.getProject projectId, {_id: 1, name: 1}, (err, project) ->
if err?
logger.err {projectId, email}, "error getting project"
return callback(err)
if !project?
logger.log {projectId}, "no project found while sending notification, returning"
return callback(null)
NotificationsBuilder.projectInvite(invite, project, sendingUser, existingUser).create(callback)
_tryCancelInviteNotification: (inviteId, callback=()->) ->
NotificationsBuilder.projectInvite({_id: inviteId}, null, null, null).read(callback)
_sendMessages: (projectId, sendingUser, invite, callback=(err)->) ->
logger.log {projectId, inviteId: invite._id}, "sending notification and email for invite"
CollaboratorsEmailHandler.notifyUserOfProjectInvite projectId, invite.email, invite, (err)->
return callback(err) if err?
CollaboratorsInviteHandler._trySendInviteNotification projectId, sendingUser, invite, (err)->
return callback(err) if err?
callback()
inviteToProject: (projectId, sendingUser, email, privileges, callback=(err,invite)->) ->
logger.log {projectId, sendingUserId: sendingUser._id, email, privileges}, "adding invite"
Crypto.randomBytes 24, (err, buffer) ->
if err?
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error generating random token"
return callback(err)
token = buffer.toString('hex')
invite = new ProjectInvite {
email: email
token: token
sendingUserId: sendingUser._id
projectId: projectId
privileges: privileges
}
invite.save (err, invite) ->
if err?
logger.err {err, projectId, sendingUserId: sendingUser._id, email}, "error saving token"
return callback(err)
# Send email and notification in background
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
logger.err {projectId, email}, "error sending messages for invite"
callback(null, invite)
revokeInvite: (projectId, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "removing invite"
ProjectInvite.remove {projectId: projectId, _id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification(inviteId, ()->)
callback(null)
resendInvite: (projectId, sendingUser, inviteId, callback=(err)->) ->
logger.log {projectId, inviteId}, "resending invite email"
ProjectInvite.findOne {_id: inviteId, projectId: projectId}, (err, invite) ->
if err?
logger.err {err, projectId, inviteId}, "error finding invite"
return callback(err)
if !invite?
logger.err {err, projectId, inviteId}, "no invite found, nothing to resend"
return callback(null)
CollaboratorsInviteHandler._sendMessages projectId, sendingUser, invite, (err) ->
if err?
logger.err {projectId, inviteId}, "error resending invite messages"
return callback(err)
callback(null)
getInviteByToken: (projectId, tokenString, callback=(err,invite)->) ->
logger.log {projectId, tokenString}, "fetching invite by token"
ProjectInvite.findOne {projectId: projectId, token: tokenString}, (err, invite) ->
if err?
logger.err {err, projectId}, "error fetching invite"
return callback(err)
if !invite?
logger.err {err, projectId, token: tokenString}, "no invite found"
return callback(null, null)
callback(null, invite)
acceptInvite: (projectId, tokenString, user, callback=(err)->) ->
logger.log {projectId, userId: user._id, tokenString}, "accepting invite"
CollaboratorsInviteHandler.getInviteByToken projectId, tokenString, (err, invite) ->
if err?
logger.err {err, projectId, tokenString}, "error finding invite"
return callback(err)
if !invite
err = new Errors.NotFoundError("no matching invite found")
logger.log {err, projectId, tokenString}, "no matching invite found"
return callback(err)
inviteId = invite._id
CollaboratorsHandler.addUserIdToProject projectId, invite.sendingUserId, user._id, invite.privileges, (err) ->
if err?
logger.err {err, projectId, inviteId, userId: user._id}, "error adding user to project"
return callback(err)
# Remove invite
logger.log {projectId, inviteId}, "removing invite"
ProjectInvite.remove {_id: inviteId}, (err) ->
if err?
logger.err {err, projectId, inviteId}, "error removing invite"
return callback(err)
CollaboratorsInviteHandler._tryCancelInviteNotification inviteId, ()->
callback()
@@ -1,6 +1,8 @@
CollaboratorsController = require('./CollaboratorsController')
AuthenticationController = require('../Authentication/AuthenticationController')
AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
CollaboratorsInviteController = require('./CollaboratorsInviteController')
RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
module.exports =
apply: (webRouter, apiRouter) ->
@@ -8,3 +10,63 @@ module.exports =
webRouter.post '/project/:Project_id/users', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.addUserToProject
webRouter.delete '/project/:Project_id/users/:user_id', AuthorizationMiddlewear.ensureUserCanAdminProject, CollaboratorsController.removeUserFromProject
webRouter.get(
'/project/:Project_id/members',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsController.getAllMembers
)
# invites
webRouter.post(
'/project/:Project_id/invite',
RateLimiterMiddlewear.rateLimit({
endpointName: "invite-to-project"
params: ["Project_id"]
maxRequests: 200
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.inviteToProject
)
webRouter.get(
'/project/:Project_id/invites',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.getAllInvites
)
webRouter.delete(
'/project/:Project_id/invite/:invite_id',
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.revokeInvite
)
webRouter.post(
'/project/:Project_id/invite/:invite_id/resend',
RateLimiterMiddlewear.rateLimit({
endpointName: "resend-invite"
params: ["Project_id"]
maxRequests: 200
timeInterval: 60 * 10
}),
AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanAdminProject,
CollaboratorsInviteController.resendInvite
)
webRouter.get(
'/project/:Project_id/invite/token/:token',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.viewInvite
)
webRouter.post(
'/project/:Project_id/invite/token/:token/accept',
AuthenticationController.requireLogin(),
CollaboratorsInviteController.acceptInvite
)
@@ -48,6 +48,10 @@ module.exports = ClsiCookieManager =
multi.exec (err)->
callback(err, serverId)
clearServerId: (project_id, callback = (err)->)->
if !clsiCookiesEnabled
return callback()
rclient.del buildKey(project_id), callback
getCookieJar: (project_id, callback = (err, jar)->)->
if !clsiCookiesEnabled
@@ -147,6 +147,7 @@ module.exports = ClsiManager =
timeout: options.timeout
imageName: project.imageName
draft: !!options.draft
check: options.check
rootResourcePath: rootResourcePath
resources: resources
}
@@ -16,51 +16,53 @@ module.exports = CompileController =
res.setTimeout(5 * 60 * 1000)
project_id = req.params.Project_id
isAutoCompile = !!req.query?.auto_compile
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
user_id = AuthenticationController.getLoggedInUserId req
options = {
isAutoCompile: isAutoCompile
}
if req.body?.rootDoc_id?
options.rootDoc_id = req.body.rootDoc_id
else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.body?.check in ['validate', 'error', 'silent']
options.check = req.body.check
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
options = {
isAutoCompile: isAutoCompile
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
compileGroup: limits?.compileGroup
clsiServerId:clsiServerId
validationProblems:validationProblems
}
if req.body?.rootDoc_id?
options.rootDoc_id = req.body.rootDoc_id
else if req.body?.settingsOverride?.rootDoc_id? # Can be removed after deploy
options.rootDoc_id = req.body.settingsOverride.rootDoc_id
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
compileGroup: limits?.compileGroup
clsiServerId:clsiServerId
validationProblems:validationProblems
}
stopCompile: (req, res, next = (error) ->) ->
project_id = req.params.Project_id
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
user_id = AuthenticationController.getLoggedInUserId req
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
CompileManager.stopCompile project_id, user_id, (error) ->
return next(error) if error?
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
CompileManager.stopCompile project_id, user_id, (error) ->
return next(error) if error?
res.status(200).send()
res.status(200).send()
_compileAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
user_id = AuthenticationController.getLoggedInUserId req
return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
_downloadAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
user_id = AuthenticationController.getLoggedInUserId req
return callback(null, user_id)
else
callback() # do a per-project compile, not per-user
@@ -149,9 +151,9 @@ module.exports = CompileController =
{page, h, v} = req.query
if not page?.match(/^\d+$/)
return next(new Error("invalid page parameter"))
if not h?.match(/^\d+\.\d+$/)
if not h?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid h parameter"))
if not v?.match(/^\d+\.\d+$/)
if not v?.match(/^-?\d+\.\d+$/)
return next(new Error("invalid v parameter"))
# whether this request is going to a per-user container
CompileController._compileAsUser req, (error, user_id) ->
@@ -6,33 +6,32 @@ Modules = require "../../infrastructure/Modules"
module.exports = ContactsController =
getContacts: (req, res, next) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
user_id = AuthenticationController.getLoggedInUserId req
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
return next(error) if error?
ContactManager.getContactIds user_id, {limit: 50}, (error, contact_ids) ->
UserGetter.getUsers contact_ids, {
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
}, (error, contacts) ->
return next(error) if error?
UserGetter.getUsers contact_ids, {
email: 1, first_name: 1, last_name: 1, holdingAccount: 1
}, (error, contacts) ->
return next(error) if error?
# UserGetter.getUsers may not preserve order so put them back in order
positions = {}
for contact_id, i in contact_ids
positions[contact_id] = i
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
contacts = contacts.filter (c) -> !c.holdingAccount
contacts = contacts.map(ContactsController._formatContact)
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
return next(error) if error?
contacts = contacts.concat(additional_contacts...)
res.send({
contacts: contacts
})
# UserGetter.getUsers may not preserve order so put them back in order
positions = {}
for contact_id, i in contact_ids
positions[contact_id] = i
contacts.sort (a,b) -> positions[a._id?.toString()] - positions[b._id?.toString()]
# Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
contacts = contacts.filter (c) -> !c.holdingAccount
contacts = contacts.map(ContactsController._formatContact)
Modules.hooks.fire "getContacts", user_id, contacts, (error, additional_contacts) ->
return next(error) if error?
contacts = contacts.concat(additional_contacts...)
res.send({
contacts: contacts
})
_formatContact: (contact) ->
return {
id: contact._id?.toString()
@@ -9,6 +9,7 @@ AuthorizationManager = require("../Authorization/AuthorizationManager")
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
Metrics = require('../../infrastructure/Metrics')
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInviteHandler = require("../Collaborators/CollaboratorsInviteHandler")
PrivilegeLevels = require "../Authorization/PrivilegeLevels"
module.exports = EditorHttpController =
@@ -30,6 +31,7 @@ module.exports = EditorHttpController =
ProjectDeleter.unmarkAsDeletedByExternalSource project_id
_buildJoinProjectView: (project_id, user_id, callback = (error, project, privilegeLevel) ->) ->
logger.log {project_id, user_id}, "building the joinProject view"
ProjectGetter.getProjectWithoutDocLines project_id, (error, project) ->
return callback(error) if error?
return callback(new Error("not found")) if !project?
@@ -40,10 +42,13 @@ module.exports = EditorHttpController =
AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, (error, privilegeLevel) ->
return callback(error) if error?
if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
callback null, null, false
else
logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
return callback null, null, false
CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
return callback(error) if error?
logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
callback(null,
ProjectEditorHandler.buildProjectModelView(project, members),
ProjectEditorHandler.buildProjectModelView(project, members, invites),
privilegeLevel
)
@@ -135,5 +140,3 @@ module.exports = EditorHttpController =
EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) ->
return next(error) if error?
res.sendStatus 204
@@ -7,7 +7,7 @@ settings = require("settings-sharelatex")
templates = {}
templates.registered =
templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
@@ -19,14 +19,14 @@ templates.registered =
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
"""
templates.canceledSubscription =
templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
compiledTemplate: _.template '''
<p>Hi <%= first_name %>,</p>
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/F7OzIY">this survey</a>?</p>
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/f5lBiZ">this survey</a>?</p>
<p>Thank you in advance.</p>
@@ -36,7 +36,7 @@ ShareLaTeX Co-founder
</p>
'''
templates.passwordResetRequested =
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout
type:"notification"
@@ -66,7 +66,7 @@ If you didn't request a password reset, let us know.
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.projectSharedWithYou =
templates.projectSharedWithYou =
subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
layout: NotificationEmailLayout
type:"notification"
@@ -87,8 +87,33 @@ templates.projectSharedWithYou =
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: NotificationEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
templates.completeJoinGroupAccount =
Follow this link to view the project: <%= inviteUrl %>
Thank you
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: _.template """
<p>Hi, <%= owner.email %> wants to share <a href="<%= inviteUrl %>">'<%= project.name %>'</a> with you</p>
<center>
<a style="text-decoration: none; width: 200px; background-color: #a93629; border: 1px solid #e24b3b; border-radius: 3px; padding: 15px; margin: 24px; display: block;" href="<%= inviteUrl %>" style="text-decoration:none" target="_blank">
<span style= "font-size:16px;font-family:Helvetica,Arial;font-weight:400;color:#fff;white-space:nowrap;display:block; text-align:center">
View Project
</span>
</a>
</center>
<p> Thank you</p>
<p> <a href="<%= siteUrl %>">#{settings.appName}</a></p>
"""
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout
type:"notification"
@@ -121,6 +146,6 @@ module.exports =
return {
subject : template.subject(opts)
html: template.layout(opts)
text: template?.plainTextTemplate?(opts)
type:template.type
}
@@ -3,7 +3,7 @@ EmailBuilder = require "./EmailBuilder"
EmailSender = require "./EmailSender"
if !settings.email?
settings.email =
settings.email =
lifecycleEnabled:false
@@ -14,6 +14,7 @@ module.exports =
if email.type == "lifecycle" and !settings.email.lifecycle
return callback()
opts.html = email.html
opts.text = email.text
opts.subject = email.subject
EmailSender.sendEmail opts, (err)->
callback(err)
callback(err)
@@ -49,8 +49,11 @@ module.exports =
from: defaultFromAddress
subject: options.subject
html: options.html
text: options.text
replyTo: options.replyTo || Settings.email.replyToAddress
socketTimeout: 30 * 1000
if Settings.email.textEncoding?
opts.textEncoding = textEncoding
client.sendMail options, (err, res)->
if err?
logger.err err:err, "error sending message"
@@ -1,5 +1,6 @@
Errors = require "./Errors"
logger = require "logger-sharelatex"
AuthenticationController = require '../Authentication/AuthenticationController'
module.exports = ErrorController =
notFound: (req, res)->
@@ -11,15 +12,16 @@ module.exports = ErrorController =
res.status(500)
res.render 'general/500',
title: "Server Error"
handleError: (error, req, res, next) ->
user = AuthenticationController.getSessionUser(req)
if error?.code is 'EBADCSRFTOKEN'
logger.warn err: error,url:req.url, method:req.method, user:req?.sesson?.user, "invalid csrf"
logger.warn err: error,url:req.url, method:req.method, user:user, "invalid csrf"
res.sendStatus(403)
return
if error instanceof Errors.NotFoundError
logger.warn {err: error, url: req.url}, "not found error"
ErrorController.notFound req, res
else
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
ErrorController.serverError req, res
logger.error err: error, url:req.url, method:req.method, user:user, "error passed to top level next middlewear"
ErrorController.serverError req, res
@@ -0,0 +1,11 @@
mimelib = require("mimelib")
module.exports = EmailHelper =
parseEmail: (email) ->
email = mimelib.parseAddresses(email or "")[0]?.address?.toLowerCase()
if !email? or email == ""
return null
else
return email
@@ -1,16 +1,31 @@
logger = require("logger-sharelatex")
NotificationsHandler = require("./NotificationsHandler")
module.exports =
module.exports =
# Note: notification keys should be url-safe
groupPlan: (user, licence)->
key : "join-sub-#{licence.subscription_id}"
create: (callback = ->)->
messageOpts =
messageOpts =
groupName: licence.name
subscription_id: licence.subscription_id
logger.log user_id:user._id, key:key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
logger.log user_id:user._id, key:@key, "creating notification key for user"
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, null, callback
read: (callback = ->)->
NotificationsHandler.markAsReadWithKey user._id, @key, callback
projectInvite: (invite, project, sendingUser, user) ->
key: "project-invite-#{invite._id}"
create: (callback=()->) ->
messageOpts =
userName: sendingUser.first_name
projectName: project.name
projectId: project._id.toString()
token: invite.token
logger.log {user_id: user._id, project_id: project._id, invite_id: invite._id, key: @key}, "creating project invite notification for user"
NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
read: (callback=()->) ->
NotificationsHandler.markAsReadByKeyOnly @key, callback
@@ -1,18 +1,20 @@
NotificationsHandler = require("./NotificationsHandler")
AuthenticationController = require("../Authentication/AuthenticationController")
logger = require("logger-sharelatex")
_ = require("underscore")
module.exports =
getAllUnreadNotifications: (req, res)->
NotificationsHandler.getUserNotifications req.session.user._id, (err, unreadNotifications)->
unreadNotifications = _.map unreadNotifications, (notification)->
user_id = AuthenticationController.getLoggedInUserId(req)
NotificationsHandler.getUserNotifications user_id, (err, unreadNotifications)->
unreadNotifications = _.map unreadNotifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
res.send(unreadNotifications)
markNotificationAsRead: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
notification_id = req.params.notification_id
NotificationsHandler.markAsRead user_id, notification_id, ->
res.send()
@@ -10,10 +10,10 @@ makeRequest = (opts, callback)->
else
request(opts, callback)
module.exports =
module.exports =
getUserNotifications: (user_id, callback)->
opts =
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
json: true
timeout: oneSecond
@@ -29,21 +29,25 @@ module.exports =
unreadNotifications = []
callback(null, unreadNotifications)
createNotification: (user_id, key, templateKey, messageOpts, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
json: {
createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
payload = {
key:key
messageOpts:messageOpts
templateKey:templateKey
}
forceCreate: true
}
if expiryDateTime?
payload.expires = expiryDateTime
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
timeout: oneSecond
method:"POST"
json: payload
logger.log opts:opts, "creating notification for user"
makeRequest opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
opts =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
method: "DELETE"
timeout: oneSecond
@@ -52,7 +56,7 @@ module.exports =
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
makeRequest opts, callback
markAsRead: (user_id, notification_id, callback)->
opts =
@@ -61,3 +65,13 @@ module.exports =
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
makeRequest opts, callback
# removes notification by key, without regard for user_id,
# should not be exposed to user via ui/router
markAsReadByKeyOnly: (key, callback)->
opts =
uri: "#{settings.apis.notifications?.url}/key/#{key}"
method: "DELETE"
timeout: oneSecond
logger.log {key:key}, "sending mark notification as read with key-only to notifications api"
makeRequest opts, callback
@@ -18,6 +18,8 @@ InactiveProjectManager = require("../InactiveData/InactiveProjectManager")
ProjectUpdateHandler = require("./ProjectUpdateHandler")
ProjectGetter = require("./ProjectGetter")
PrivilegeLevels = require("../Authorization/PrivilegeLevels")
AuthenticationController = require("../Authentication/AuthenticationController")
PackageVersions = require("../../infrastructure/PackageVersions")
module.exports = ProjectController =
@@ -45,10 +47,10 @@ module.exports = ProjectController =
async.series jobs, (error) ->
return next(error) if error?
res.sendStatus(204)
updateProjectAdminSettings: (req, res, next) ->
project_id = req.params.Project_id
jobs = []
if req.body.publicAccessLevel?
jobs.push (callback) ->
@@ -88,32 +90,33 @@ module.exports = ProjectController =
project_id = req.params.Project_id
projectName = req.body.projectName
logger.log project_id:project_id, projectName:projectName, "cloning project"
if !req.session.user?
if !AuthenticationController.isUserLoggedIn(req)
return res.send redir:"/register"
projectDuplicator.duplicate req.session.user, project_id, projectName, (err, project)->
currentUser = AuthenticationController.getSessionUser(req)
projectDuplicator.duplicate currentUser, project_id, projectName, (err, project)->
if err?
logger.error err:err, project_id: project_id, user_id: req.session.user._id, "error cloning project"
logger.error err:err, project_id: project_id, user_id: currentUser._id, "error cloning project"
return next(err)
res.send(project_id:project._id)
newProject: (req, res)->
user = req.session.user
user_id = AuthenticationController.getLoggedInUserId(req)
projectName = req.body.projectName?.trim()
template = req.body.template
logger.log user: user, projectType: template, name: projectName, "creating project"
logger.log user: user_id, projectType: template, name: projectName, "creating project"
async.waterfall [
(cb)->
if template == 'example'
projectCreationHandler.createExampleProject user._id, projectName, cb
projectCreationHandler.createExampleProject user_id, projectName, cb
else
projectCreationHandler.createBasicProject user._id, projectName, cb
projectCreationHandler.createBasicProject user_id, projectName, cb
], (err, project)->
if err?
logger.error err: err, project: project, user: user, name: projectName, templateType: template, "error creating project"
logger.error err: err, project: project, user: user_id, name: projectName, templateType: template, "error creating project"
res.sendStatus 500
else
logger.log project: project, user: user, name: projectName, templateType: template, "created project"
logger.log project: project, user: user_id, name: projectName, templateType: template, "created project"
res.send {project_id:project._id}
@@ -131,7 +134,8 @@ module.exports = ProjectController =
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
currentUser = AuthenticationController.getSessionUser(req)
async.parallel {
tags: (cb)->
TagsHandler.getAllTags user_id, cb
@@ -140,7 +144,7 @@ module.exports = ProjectController =
projects: (cb)->
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
user: (cb) ->
User.findById user_id, "featureSwitches", cb
}, (err, results)->
@@ -149,7 +153,7 @@ module.exports = ProjectController =
return next(err)
logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0]
notifications = require("underscore").map results.notifications, (notification)->
notifications = require("underscore").map results.notifications, (notification)->
notification.html = req.i18n.translate(notification.templateKey, notification.messageOpts)
return notification
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
@@ -183,8 +187,8 @@ module.exports = ProjectController =
if !Settings.editorIsOpen
return res.render("general/closed", {title:"updating_site"})
if req.session.user?
user_id = req.session.user._id
if AuthenticationController.isUserLoggedIn(req)
user_id = AuthenticationController.getLoggedInUserId(req)
anonymous = false
else
anonymous = true
@@ -259,6 +263,7 @@ module.exports = ProjectController =
fontSize : user.ace.fontSize
autoComplete: user.ace.autoComplete
pdfViewer : user.ace.pdfViewer
syntaxValidation: user.ace.syntaxValidation
}
privilegeLevel: privilegeLevel
chatUrl: Settings.apis.chat.url
@@ -320,6 +325,7 @@ defaultSettingsForAnonymousUser = (user_id)->
autoComplete: true
spellCheckLanguage: ""
pdfViewer: ""
syntaxValidation: true
subscription:
freeTrial:
allowed: true
@@ -328,8 +334,8 @@ defaultSettingsForAnonymousUser = (user_id)->
THEME_LIST = []
do generateThemeList = () ->
files = fs.readdirSync __dirname + '/../../../../public/js/ace'
files = fs.readdirSync __dirname + '/../../../../public/js/' + PackageVersions.lib('ace')
for file in files
if file.slice(-2) == "js" and file.match(/^theme-/)
cleanName = file.slice(0,-3).slice(6)
THEME_LIST.push cleanName
THEME_LIST.push cleanName
@@ -1,7 +1,7 @@
_ = require("underscore")
module.exports = ProjectEditorHandler =
buildProjectModelView: (project, members) ->
buildProjectModelView: (project, members, invites) ->
result =
_id : project._id
name : project.name
@@ -15,17 +15,16 @@ module.exports = ProjectEditorHandler =
deletedByExternalDataSource : project.deletedByExternalDataSource || false
deletedDocs: project.deletedDocs
members: []
owner = null
for member in members
if member.privilegeLevel == "owner"
owner = member.user
else
result.members.push @buildUserModelView member.user, member.privilegeLevel
if owner?
result.owner = @buildUserModelView owner, "owner"
invites: invites
result.features = _.defaults(owner?.features or {}, {
if !result.invites?
result.invites = []
{owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members)
result.owner = owner
result.members = members
result.features = _.defaults(ownerFeatures or {}, {
collaborators: -1 # Infinite
versioning: false
dropbox:false
@@ -37,6 +36,18 @@ module.exports = ProjectEditorHandler =
return result
buildOwnerAndMembersViews: (members) ->
owner = null
ownerFeatures = null
filteredMembers = []
for member in members
if member.privilegeLevel == "owner"
ownerFeatures = member.user.features
owner = @buildUserModelView member.user, "owner"
else
filteredMembers.push @buildUserModelView member.user, member.privilegeLevel
{owner: owner, ownerFeatures: ownerFeatures, members: filteredMembers}
buildUserModelView: (user, privileges) ->
_id : user._id
first_name : user.first_name
@@ -258,6 +258,7 @@ module.exports = ProjectEntityHandler =
if !foundFolder?
logger.log path:path, project_id:project._id, folderName:folderName, "making folder from mkdirp"
@addFolder project_id, parentFolder_id, folderName, (err, newFolder, parentFolder_id)->
return callback(err) if err?
newFolder.parentFolder_id = parentFolder_id
previousFolders.push newFolder
callback null, previousFolders
@@ -268,6 +269,7 @@ module.exports = ProjectEntityHandler =
async.reduce folders, [], procesFolder, (err, folders)->
return callback(err) if err?
lastFolder = folders[folders.length-1]
folders = _.select folders, (folder)->
!folder.filterOut
@@ -81,7 +81,7 @@ module.exports = ProjectLocator =
else
getRootDoc project
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity)->)->
findElementByPath: (project_or_id, needlePath, callback = (err, foundEntity, type)->)->
getParentFolder = (haystackFolder, foldersList, level, cb)->
if foldersList.length == 0
@@ -100,12 +100,22 @@ module.exports = ProjectLocator =
getEntity = (folder, entityName, cb)->
if !entityName?
return cb null, folder
enteties = _.union folder.fileRefs, folder.docs, folder.folders
result = _.find enteties, (entity)->
entity?.name.toLowerCase() == entityName.toLowerCase()
return cb null, folder, "folder"
for file in folder.fileRefs or []
if file?.name.toLowerCase() == entityName.toLowerCase()
result = file
type = "file"
for doc in folder.docs or []
if doc?.name.toLowerCase() == entityName.toLowerCase()
result = doc
type = "doc"
for childFolder in folder.folders or []
if childFolder?.name.toLowerCase() == entityName.toLowerCase()
result = childFolder
type = "folder"
if result?
cb null, result
cb null, result, type
else
cb("not found project_or_id: #{project_or_id} search path: #{needlePath}, entity #{entityName} could not be found")
@@ -117,7 +127,7 @@ module.exports = ProjectLocator =
if !project?
return callback("project could not be found for finding a element #{project_or_id}")
if needlePath == '' || needlePath == '/'
return callback(null, project.rootFolder[0])
return callback(null, project.rootFolder[0], "folder")
if needlePath.indexOf('/') == 0
needlePath = needlePath.substring(1)
@@ -1,9 +1,11 @@
logger = require('logger-sharelatex')
ReferalHandler = require('./ReferalHandler')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
module.exports =
bonus: (req, res)->
ReferalHandler.getReferedUserIds req.session.user._id, (err, refered_users)->
user_id = AuthenticationController.getLoggedInUserId(req)
ReferalHandler.getReferedUserIds user_id, (err, refered_users)->
res.render "referal/bonus",
title: "bonus_please_recommend_us"
refered_users: refered_users
@@ -1,11 +0,0 @@
User = require("../../models/User").User
module.exports = RefererMiddleware =
getUserReferalId: (req, res, next) ->
if req.session? and req.session.user?
User.findById req.session.user._id, (error, user) ->
return next(error) if error?
req.session.user.referal_id = user.referal_id
next()
else
next()
@@ -4,7 +4,7 @@ settings = require("settings-sharelatex")
ProjectGetter = require "../Project/ProjectGetter"
UserGetter = require "../User/UserGetter"
DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
U = require('underscore')
_ = require('underscore')
Async = require('async')
oneMinInMs = 60 * 1000
@@ -22,24 +22,24 @@ module.exports = ReferencesHandler =
_findBibFileIds: (project) ->
ids = []
_process = (folder) ->
(folder.fileRefs or []).forEach (file) ->
_.each (folder.fileRefs or []), (file) ->
if file?.name?.match(/^.*\.bib$/)
ids.push(file._id)
(folder.folders or []).forEach (folder) ->
_.each (folder.folders or []), (folder) ->
_process(folder)
(project.rootFolder or []).forEach (rootFolder) ->
_.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
_findBibDocIds: (project) ->
ids = []
_process = (folder) ->
(folder.docs or []).forEach (doc) ->
_.each (folder.docs or []), (doc) ->
if doc?.name?.match(/^.*\.bib$/)
ids.push(doc._id)
(folder.folders or []).forEach (folder) ->
_.each (folder.folders or []), (folder) ->
_process(folder)
(project.rootFolder or []).forEach (rootFolder) ->
_.each (project.rootFolder or []), (rootFolder) ->
_process(rootFolder)
return ids
@@ -30,5 +30,5 @@ module.exports =
multi.get buildKey(token)
multi.del buildKey(token)
multi.exec (err, results)->
callback err, results[0]
callback err, results?[0]
@@ -1,24 +1,22 @@
RateLimiter = require "../../infrastructure/RateLimiter"
logger = require "logger-sharelatex"
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = RateLimiterMiddlewear =
###
Do not allow more than opts.maxRequests from a single client in
opts.timeInterval. Pass an array of opts.params to segment this based on
parameters in the request URL, e.g.:
app.get "/project/:project_id", RateLimiterMiddlewear.rateLimit(endpointName: "open-editor", params: ["project_id"])
will rate limit each project_id separately.
Unique clients are identified by user_id if logged in, and IP address if not.
###
rateLimit: (opts) ->
return (req, res, next) ->
if req.session?.user?
user_id = req.session.user._id
else
user_id = req.ip
user_id = AuthenticationController.getLoggedInUserId(req) || req.ip
params = (opts.params or []).map (p) -> req.params[p]
params.push user_id
if !opts.endpointName?
@@ -37,4 +35,4 @@ module.exports = RateLimiterMiddlewear =
logger.warn options, "rate limit exceeded"
res.status(429) # Too many requests
res.write("Rate limit reached, please try again later")
res.end()
res.end()
@@ -1,13 +1,15 @@
request = require 'request'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
AuthenticationController = require('../Authentication/AuthenticationController')
TEN_SECONDS = 1000 * 10
module.exports = SpellingController =
proxyRequestToSpellingApi: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
url = req.url.slice("/spelling".length)
url = "/user/#{req.session.user._id}#{url}"
url = "/user/#{user_id}#{url}"
req.headers["Host"] = Settings.apis.spelling.host
request(url: Settings.apis.spelling.url + url, method: req.method, headers: req.headers, json: req.body, timeout:TEN_SECONDS)
.on "error", (error) ->
@@ -5,12 +5,13 @@ Path = require "path"
fs = require "fs"
ErrorController = require "../Errors/ErrorController"
AuthenticationController = require('../Authentication/AuthenticationController')
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.jade")
module.exports = HomeController =
index : (req,res)->
if req.session.user
if AuthenticationController.isUserLoggedIn(req)
if req.query.scribtex_path?
res.redirect "/project?scribtex_path=#{req.query.scribtex_path}"
else
@@ -33,4 +34,4 @@ module.exports = HomeController =
res.render "external/#{page}.jade",
title: title
else
ErrorController.notFound(req, res, next)
ErrorController.notFound(req, res, next)
@@ -1,70 +1,16 @@
request = require("request")
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
_ = require("underscore")
ErrorController = require "../Errors/ErrorController"
StaticPageHelpers = require("./StaticPageHelpers")
sanitize = require('sanitizer')
Settings = require("settings-sharelatex")
contentful = require('contentful')
marked = require("marked")
sixpack = require("../../infrastructure/Sixpack")
module.exports = UniversityController =
module.exports = UniversityController =
getPage: (req, res, next)->
url = req.url?.toLowerCase()
universityUrl = "#{settings.apis.university.url}#{url}"
if StaticPageHelpers.shouldProxy(url)
return UniversityController._directProxy universityUrl, res
logger.log url:url, "proxying request to university api"
request.get universityUrl, (err, r, data)->
if r?.statusCode == 404
return UniversityController.getContentfulPage(req, res, next)
if err?
return res.send 500
data = data.trim()
try
data = JSON.parse(data)
data.content = data.content.replace(/__ref__/g, sanitize.escape(req.query.ref))
catch err
logger.err err:err, data:data, "error parsing data from data"
res.render "university/university_holder", data
url = req.url?.toLowerCase().replace(".html","")
return res.redirect("/i#{url}")
getIndexPage: (req, res)->
req.url = "/university/index.html"
UniversityController.getPage req, res
_directProxy: (originUrl, res)->
upstream = request.get(originUrl)
upstream.on "error", (error) ->
logger.error err: error, "university proxy error"
upstream.pipe res
getContentfulPage: (req, res, next)->
console.log Settings.contentful
if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken?
return ErrorController.notFound(req, res, next)
client = contentful.createClient({
space: Settings.contentful?.uni?.space
accessToken: Settings.contentful?.uni?.accessToken
})
url = req.url?.toLowerCase().replace("/university/","")
client.getEntries({content_type: 'caseStudy', 'fields.slug':url})
.catch (e)->
return res.send 500
.then (entry)->
if !entry? or !entry.items? or entry.items.length == 0
return ErrorController.notFound(req, res, next)
viewData = entry.items[0].fields
viewData.html = marked(viewData.content)
res.render "university/case_study", viewData:viewData
return res.redirect("/i/university")
@@ -4,6 +4,7 @@ User = require("../../models/User").User
SubscriptionLocator = require("./SubscriptionLocator")
Settings = require("settings-sharelatex")
CollaboratorsHandler = require("../Collaborators/CollaboratorsHandler")
CollaboratorsInvitesHandler = require("../Collaborators/CollaboratorsInviteHandler")
module.exports =
@@ -20,10 +21,12 @@ module.exports =
return callback(error) if error?
CollaboratorsHandler.getCollaboratorCount project_id, (error, current_number) =>
return callback(error) if error?
if current_number + x_collaborators <= allowed_number or allowed_number < 0
callback null, true
else
callback null, false
CollaboratorsInvitesHandler.getInviteCount project_id, (error, invite_count) =>
return callback(error) if error?
if current_number + invite_count + x_collaborators <= allowed_number or allowed_number < 0
callback null, true
else
callback null, false
userHasSubscriptionOrIsGroupMember: (user, callback = (err, hasSubscriptionOrIsMember)->) ->
@userHasSubscription user, (err, hasSubscription, subscription)=>
@@ -41,7 +44,7 @@ module.exports =
hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
callback err, hasValidSubscription, subscription
userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
logger.log user_id: user._id, "checking is user is member of subscription groups"
SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
@@ -65,4 +68,3 @@ getOwnerOfProject = (project_id, callback)->
return callback(error) if error?
User.findById project.owner_ref, (error, owner) ->
callback(error, owner)
@@ -29,13 +29,15 @@ module.exports = RecurlyWrapper =
RecurlyWrapper.apiRequest({
url: "accounts/#{user._id}"
method: "GET"
expect404: true
}, (error, response, responseBody) ->
if error
if response.statusCode == 404 # actually not an error in this case, just no existing account
cache.userExists = false
return next(null, cache)
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
return next(error)
if response.statusCode == 404 # actually not an error in this case, just no existing account
logger.log {user_id: user._id, recurly_token_id}, "user does not currently exist in recurly, proceed"
cache.userExists = false
return next(null, cache)
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
if err
@@ -236,10 +238,14 @@ module.exports = RecurlyWrapper =
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
"Accept" : "application/xml"
"Content-Type" : "application/xml; charset=utf-8"
expect404 = options.expect404
delete options.expect404
request options, (error, response, body) ->
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204
unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 or (response.statusCode == 404 and expect404)
logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly"
error = "Recurly API returned with status code: #{response.statusCode}"
if response.statusCode == 404 and expect404
logger.log {url: options.url, method: options.method}, "got 404 response from recurly, expected as valid response"
callback(error, response, body)
sign : (parameters, callback) ->
@@ -13,15 +13,16 @@ module.exports = SubscriptionController =
plansPage: (req, res, next) ->
plans = SubscriptionViewModelBuilder.buildViewModel()
if !req.session.user?
baseUrl = "/register?redir="
else
if AuthenticationController.isUserLoggedIn(req)
baseUrl = ""
else
baseUrl = "/register?redir="
viewName = "subscriptions/plans"
if req.query.v?
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
return next(err) if err?
res.render viewName,
title: "plans_and_pricing"
plans: plans
@@ -31,200 +32,210 @@ module.exports = SubscriptionController =
#get to show the recurly.js page
paymentPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if hasSubscription or !plan?
res.redirect "/user/subscription"
else
currency = req.query.currency?.toUpperCase()
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
return next(err) if err?
if recomendedCurrency? and !currency?
currency = recomendedCurrency
RecurlyWrapper.sign {
subscription:
plan_code : req.query.planCode
user = AuthenticationController.getSessionUser(req)
plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if hasSubscription or !plan?
res.redirect "/user/subscription"
else
currency = req.query.currency?.toUpperCase()
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)->
return next(err) if err?
if recomendedCurrency? and !currency?
currency = recomendedCurrency
RecurlyWrapper.sign {
subscription:
plan_code : req.query.planCode
currency: currency
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/new",
title : "subscribe"
plan_code: req.query.planCode
currency: currency
countryCode:countryCode
plan:plan
showStudentPlan: req.query.ssp
recurlyConfig: JSON.stringify
currency: currency
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/new",
title : "subscribe"
plan_code: req.query.planCode
currency: currency
countryCode:countryCode
plan:plan
showStudentPlan: req.query.ssp
recurlyConfig: JSON.stringify
currency: currency
subdomain: Settings.apis.recurly.subdomain
showCouponField: req.query.scf
showVatField: req.query.svf
couponCode: req.query.cc or ""
subdomain: Settings.apis.recurly.subdomain
showCouponField: req.query.scf
showVatField: req.query.svf
couponCode: req.query.cc or ""
userSubscriptionPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) =>
return next(error) if error?
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
groupLicenceInviteUrl = SubscriptionDomainHandler.getDomainLicencePage(user)
if subscription?.customAccount
logger.log user: user, "redirecting to custom account page"
res.redirect "/user/subscription/custom_account"
else if groupLicenceInviteUrl? and !hasSubOrIsGroupMember
logger.log user:user, "redirecting to group subscription invite page"
res.redirect groupLicenceInviteUrl
else if !hasSubOrIsGroupMember
logger.log user: user, "redirecting to plans"
res.redirect "/user/subscription/plans"
else
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
return next(error) if error?
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
res.render "subscriptions/dashboard",
title: "your_subscription"
recomendedCurrency: subscription?.currency
taxRate:subscription?.taxRate
plans: plans
subscription: subscription || {}
groupSubscriptions: groupSubscriptions
subscriptionTabActive: true
user:user
saved_billing_details: req.query.saved_billing_details?
userCustomSubscriptionPage: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
return next(err)
res.render "subscriptions/custom_account",
title: "your_subscription"
subscription: subscription
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscriptionOrIsGroupMember user, (err, hasSubOrIsGroupMember, subscription)->
return next(err) if err?
if !subscription?
err = new Error("subscription null for custom account, user:#{user?._id}")
logger.warn err:err, "subscription is null for custom accounts page"
return next(err)
res.render "subscriptions/custom_account",
title: "your_subscription"
subscription: subscription
editBillingDetailsPage: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/update"
user :
id : user._id
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription)->
return next(err) if err?
if !hasSubscription
res.redirect "/user/subscription"
else
RecurlyWrapper.sign {
account_code: user._id
}, (error, signature) ->
return next(error) if error?
res.render "subscriptions/edit-billing-details",
title : "update_billing_details"
recurlyConfig: JSON.stringify
currency: "USD"
subdomain: Settings.apis.recurly.subdomain
signature : signature
successURL : "#{Settings.siteUrl}/user/subscription/billing-details/update"
user :
id : user._id
updateBillingDetails: (req, res, next) ->
res.redirect "/user/subscription?saved_billing_details=true"
createSubscription: (req, res, next)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return callback(error) if error?
recurly_token_id = req.body.recurly_token_id
subscriptionDetails = req.body.subscriptionDetails
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
return res.sendStatus 500
res.sendStatus 201
user = AuthenticationController.getSessionUser(req)
recurly_token_id = req.body.recurly_token_id
subscriptionDetails = req.body.subscriptionDetails
logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription"
SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong creating subscription"
return res.sendStatus 500
res.sendStatus 201
successful_subscription: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) =>
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
successful_subscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription) ->
return next(error) if error?
res.render "subscriptions/successful_subscription",
title: "thank_you"
subscription:subscription
cancelSubscription: (req, res, next) ->
AuthenticationController.getLoggedInUser req, (error, user) ->
logger.log user_id:user._id, "canceling subscription"
return next(error) if error?
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
res.redirect "/user/subscription"
updateSubscription: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) ->
return next(error) if error?
planCode = req.body.plan_code
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
res.redirect "/user/subscription"
user = AuthenticationController.getSessionUser(req)
logger.log user_id:user._id, "canceling subscription"
SubscriptionHandler.cancelSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong canceling subscription"
return next(err)
res.redirect "/user/subscription"
reactivateSubscription: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) ->
logger.log user_id:user._id, "reactivating subscription"
return next(error) if error?
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
res.redirect "/user/subscription"
updateSubscription: (req, res, next)->
_origin = req?.query?.origin || null
user = AuthenticationController.getSessionUser(req)
planCode = req.body.plan_code
if !planCode?
err = new Error('plan_code is not defined')
logger.err {user_id: user._id, err, planCode, origin: _origin, body: req.body}, "[Subscription] error in updateSubscription form"
return next(err)
logger.log planCode: planCode, user_id:user._id, "updating subscription"
SubscriptionHandler.updateSubscription user, planCode, null, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong updating subscription"
return next(err)
res.redirect "/user/subscription"
recurlyCallback: (req, res)->
reactivateSubscription: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
logger.log user_id:user._id, "reactivating subscription"
SubscriptionHandler.reactivateSubscription user, (err)->
if err?
logger.err err:err, user_id:user._id, "something went wrong reactivating subscription"
return next(err)
res.redirect "/user/subscription"
recurlyCallback: (req, res, next)->
logger.log data: req.body, "received recurly callback"
# we only care if a subscription has exipired
if req.body? and req.body["expired_subscription_notification"]?
recurlySubscription = req.body["expired_subscription_notification"].subscription
SubscriptionHandler.recurlyCallback recurlySubscription, ->
SubscriptionHandler.recurlyCallback recurlySubscription, (err)->
return next(err) if err?
res.sendStatus 200
else
res.sendStatus 200
renderUpgradeToAnnualPlanPage: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) ->
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
else if planCode?.indexOf("student") != -1
planName = "student"
else if planCode?.indexOf("collaborator") != -1
planName = "collaborator"
if !hasSubscription
return res.redirect("/user/subscription/plans")
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
res.render "subscriptions/upgradeToAnnual",
title: "Upgrade to annual"
planName: planName
renderUpgradeToAnnualPlanPage: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
planCode = subscription?.planCode.toLowerCase()
if planCode?.indexOf("annual") != -1
planName = "annual"
else if planCode?.indexOf("student") != -1
planName = "student"
else if planCode?.indexOf("collaborator") != -1
planName = "collaborator"
if !hasSubscription
return res.redirect("/user/subscription/plans")
logger.log planName:planName, user_id:user._id, "rendering upgrade to annual page"
res.render "subscriptions/upgradeToAnnual",
title: "Upgrade to annual"
planName: planName
processUpgradeToAnnualPlan: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) ->
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
processUpgradeToAnnualPlan: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
{planName} = req.body
coupon_code = Settings.coupon_codes.upgradeToAnnualPromo[planName]
annualPlanName = "#{planName}-annual"
logger.log user_id:user._id, planName:annualPlanName, "user is upgrading to annual billing with discount"
SubscriptionHandler.updateSubscription user, annualPlanName, coupon_code, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
return next(err)
res.sendStatus 200
extendTrial: (req, res, next)->
user = AuthenticationController.getSessionUser(req)
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
return next(err) if err?
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
logger.err err:err, user_id:user._id, "error updating subscription"
res.sendStatus 500
res.send 500
else
res.sendStatus 200
extendTrial: (req, res)->
AuthenticationController.getLoggedInUser req, (error, user) ->
LimitationsManager.userHasSubscription user, (err, hasSubscription, subscription)->
SubscriptionHandler.extendTrial subscription, 14, (err)->
if err?
res.send 500
else
res.send 200
res.send 200
recurlyNotificationParser: (req, res, next) ->
xml = ""
@@ -3,27 +3,28 @@ logger = require("logger-sharelatex")
SubscriptionLocator = require("./SubscriptionLocator")
ErrorsController = require("../Errors/ErrorController")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
AuthenticationController = require('../Authentication/AuthenticationController')
_ = require("underscore")
async = require("async")
module.exports =
addUserToGroup: (req, res)->
adminUserId = req.session.user._id
adminUserId = AuthenticationController.getLoggedInUserId(req)
newEmail = req.body?.email?.toLowerCase()?.trim()
logger.log adminUserId:adminUserId, newEmail:newEmail, "adding user to group subscription"
SubscriptionGroupHandler.addUserToGroup adminUserId, newEmail, (err, user)->
if err?
logger.err err:err, newEmail:newEmail, adminUserId:adminUserId, "error adding user from group"
return res.sendStatus 500
result =
result =
user:user
if err and err.limitReached
result.limitReached = true
res.json(result)
removeUserFromGroup: (req, res)->
adminUserId = req.session.user._id
adminUserId = AuthenticationController.getLoggedInUserId(req)
userToRemove_id = req.params.user_id
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
@@ -31,10 +32,10 @@ module.exports =
logger.err err:err, adminUserId:adminUserId, userToRemove_id:userToRemove_id, "error removing user from group"
return res.sendStatus 500
res.send()
removeSelfFromGroup: (req, res)->
adminUserId = req.query.admin_user_id
userToRemove_id = req.session.user._id
userToRemove_id = AuthenticationController.getLoggedInUserId(req)
logger.log adminUserId:adminUserId, userToRemove_id:userToRemove_id, "removing user from group subscription after self request"
SubscriptionGroupHandler.removeUserFromGroup adminUserId, userToRemove_id, (err)->
if err?
@@ -43,7 +44,7 @@ module.exports =
res.send()
renderSubscriptionGroupAdminPage: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
return res.redirect("/")
@@ -55,11 +56,11 @@ module.exports =
renderGroupInvitePage: (req, res)->
group_subscription_id = req.params.subscription_id
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(group_subscription_id)
if !licence?
return ErrorsController.notFound(req, res)
jobs =
jobs =
partOfGroup: (cb)->
SubscriptionGroupHandler.isUserPartOfGroup user_id, licence.group_subscription_id, cb
subscription: (cb)->
@@ -77,22 +78,26 @@ module.exports =
beginJoinGroup: (req, res)->
subscription_id = req.params.subscription_id
user_id = req.session.user._id
currentUser = AuthenticationController.getSessionUser(req)
if !currentUser?
logger.err {subscription_id}, "error getting current user"
return res.sendStatus 500
licence = SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)
if !licence?
return ErrorsController.notFound(req, res)
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, req.session.user.email, (err)->
SubscriptionGroupHandler.sendVerificationEmail subscription_id, licence.name, currentUser.email, (err)->
if err?
res.sendStatus 500
else
res.sendStatus 200
completeJoin: (req, res)->
currentUser = AuthenticationController.getSessionUser(req)
subscription_id = req.params.subscription_id
if !SubscriptionDomainHandler.findDomainLicenceBySubscriptionId(subscription_id)?
return ErrorsController.notFound(req, res)
email = req?.session?.user?.email
logger.log subscription_id:subscription_id, user_id:req?.session?.user?._id, email:email, "starting the completion of joining group"
email = currentUser?.email
logger.log subscription_id:subscription_id, user_id:currentUser?._id, email:email, "starting the completion of joining group"
SubscriptionGroupHandler.processGroupVerification email, subscription_id, req.query?.token, (err)->
if err? and err == "token_not_found"
return res.redirect "/user/subscription/#{subscription_id}/group/invited?expired=true"
@@ -109,10 +114,10 @@ module.exports =
return ErrorsController.notFound(req, res)
res.render "subscriptions/group/successful_join",
title: "Sucessfully joined group"
licenceName:licence.name
licenceName:licence.name
exportGroupCsv: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "exporting group csv"
SubscriptionLocator.getUsersSubscription user_id, (err, subscription)->
if !subscription.groupPlan
@@ -7,6 +7,8 @@ SubscriptionUpdater = require("./SubscriptionUpdater")
LimitationsManager = require('./LimitationsManager')
EmailHandler = require("../Email/EmailHandler")
Events = require "../../infrastructure/Events"
Analytics = require("../Analytics/AnalyticsManager")
module.exports =
@@ -52,6 +54,7 @@ module.exports =
setTimeout (-> EmailHandler.sendEmail "canceledSubscription", emailOpts
), ONE_HOUR_IN_MS
Events.emit "cancelSubscription", user._id
Analytics.recordEvent user._id, "subscription-canceled"
callback()
else
callback()
@@ -14,7 +14,11 @@ module.exports =
logger.log user_id:user_id, "got users subscription"
callback(err, subscription)
getMemberSubscriptions: (user_id, callback) ->
getMemberSubscriptions: (user_or_id, callback) ->
if user_or_id? and user_or_id._id?
user_id = user_or_id._id
else if user_or_id?
user_id = user_or_id
logger.log user_id: user_id, "getting users group subscriptions"
Subscription.find(member_ids: user_id).populate("admin_id").exec callback
@@ -25,4 +29,4 @@ module.exports =
Subscription.findOne {member_ids: user_id, _id:subscription_id}, {_id:1}, callback
getGroupSubscriptionMemberOf: (user_id, callback)->
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
Subscription.findOne {member_ids: user_id}, {_id:1, planCode:1}, callback
@@ -16,6 +16,7 @@ module.exports =
webRouter.get '/user/subscription/new', AuthenticationController.requireLogin(), SubscriptionController.paymentPage
webRouter.get '/user/subscription/billing-details/edit', AuthenticationController.requireLogin(), SubscriptionController.editBillingDetailsPage
webRouter.post '/user/subscription/billing-details/update', AuthenticationController.requireLogin(), SubscriptionController.updateBillingDetails
webRouter.get '/user/subscription/thank-you', AuthenticationController.requireLogin(), SubscriptionController.successful_subscription
@@ -4,6 +4,7 @@ PlansLocator = require("./PlansLocator")
SubscriptionFormatters = require("./SubscriptionFormatters")
LimitationsManager = require("./LimitationsManager")
SubscriptionLocator = require("./SubscriptionLocator")
logger = require('logger-sharelatex')
_ = require("underscore")
module.exports =
@@ -16,6 +17,10 @@ module.exports =
if subscription?
return callback(error) if error?
plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
if !plan?
err = new Error("No plan found for planCode '#{subscription.planCode}'")
logger.error {user_id: user._id, err}, "error getting subscription plan for user"
return callback(err)
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
tax = recurlySubscription?.tax_in_cents || 0
callback null, {
@@ -1,48 +1,49 @@
TagsHandler = require("./TagsHandler")
logger = require("logger-sharelatex")
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
getAllTags: (req, res, next)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "getting tags"
TagsHandler.getAllTags user_id, (error, allTags)->
return next(error) if error?
res.json(allTags)
createTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
name = req.body.name
logger.log {user_id, name}, "creating tag"
TagsHandler.createTag user_id, name, (error, tag) ->
return next(error) if error?
res.json(tag)
addProjectToTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "adding tag to project"
TagsHandler.addProjectToTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
removeProjectFromTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{tag_id, project_id} = req.params
logger.log {user_id, tag_id, project_id}, "removing tag from project"
TagsHandler.removeProjectFromTag user_id, tag_id, project_id, (error) ->
return next(error) if error?
res.status(204).end()
deleteTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
logger.log {user_id, tag_id}, "deleting tag"
TagsHandler.deleteTag user_id, tag_id, (error) ->
return next(error) if error?
res.status(204).end()
renameTag: (req, res, next) ->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
tag_id = req.params.tag_id
name = req.body?.name
if !name?
@@ -33,16 +33,11 @@ module.exports =
self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback
deleteUpdate: (project_id, path, source, callback)->
projectLocator.findElementByPath project_id, path, (err, element)->
type = 'file'
projectLocator.findElementByPath project_id, path, (err, element, type)->
if err? || !element?
logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted"
return callback()
if element.lines?
type = 'doc'
else if element.folders?
type = 'folder'
logger.log project_id:project_id, updateType:path, updateType:type, element:element, "processing update to delete entity from tpds"
logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds"
editorController.deleteEntity project_id, element._id, type, source, (err)->
logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds"
callback()
@@ -56,12 +51,13 @@ module.exports =
return callback(err)
logger.log docLines:docLines, doc_id:doc_id, project_id:project_id, "processing doc update from tpds"
if doc_id?
editorController.setDoc project_id, doc_id, user_id, docLines, source, (err)->
callback()
editorController.setDoc project_id, doc_id, user_id, docLines, source, callback
else
setupNewEntity project_id, path, (err, folder, fileName)->
editorController.addDoc project_id, folder._id, fileName, docLines, source, (err)->
callback()
if err?
logger.err err:err, project_id:project_id, doc_id:doc_id, path:path, "error processing file"
return callback(err)
editorController.addDoc project_id, folder._id, fileName, docLines, source, callback
processFile: (project_id, file_id, fsPath, path, source, callback)->
finish = (err)->
@@ -69,10 +65,13 @@ module.exports =
callback(err)
logger.log project_id:project_id, file_id:file_id, path:path, "processing file update from tpds"
setupNewEntity project_id, path, (err, folder, fileName) =>
if file_id?
if err?
logger.err err:err, project_id:project_id, file_id:file_id, path:path, "error processing file"
return callback(err)
else if file_id?
editorController.replaceFile project_id, file_id, fsPath, source, finish
else
editorController.addFile project_id, folder._id, fileName, fsPath, source, finish
editorController.addFile project_id, folder?._id, fileName, fsPath, source, finish
writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
if !file_id?
@@ -5,17 +5,16 @@ AuthenticationController = require "../Authentication/AuthenticationController"
module.exports = TrackChangesController =
proxyToTrackChangesApi: (req, res, next = (error) ->) ->
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
return next(error) if error?
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
next(error)
user_id = AuthenticationController.getLoggedInUserId req
url = settings.apis.trackchanges.url + req.url
logger.log url: url, "proxying to track-changes api"
getReq = request(
url: url
method: req.method
headers:
"X-User-Id": user_id
)
getReq.pipe(res)
getReq.on "error", (error) ->
logger.error err: error, "track-changes API error"
next(error)
@@ -4,11 +4,12 @@ fs = require "fs"
Path = require "path"
FileSystemImportManager = require "./FileSystemImportManager"
ProjectUploadManager = require "./ProjectUploadManager"
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = ProjectUploadController =
uploadProject: (req, res, next) ->
timer = new metrics.Timer("project-upload")
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
{originalname, path} = req.files.qqfile
name = Path.basename(originalname, ".zip")
ProjectUploadManager.createProjectFromZipArchive user_id, name, path, (error, project) ->
@@ -24,18 +25,18 @@ module.exports = ProjectUploadController =
project: project._id, file_path: path, file_name: name,
"uploaded project"
res.send success: true, project_id: project._id
uploadFile: (req, res, next) ->
timer = new metrics.Timer("file-upload")
name = req.files.qqfile.originalname
path = req.files.qqfile.path
name = req.files.qqfile?.originalname
path = req.files.qqfile?.path
project_id = req.params.Project_id
folder_id = req.query.folder_id
if !name? or name.length == 0 or name.length > 150
logger.err project_id:project_id, name:name, "bad name when trying to upload file"
return res.send success: false
logger.log folder_id:folder_id, project_id:project_id, "getting upload file request"
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
FileSystemImportManager.addEntity user_id, project_id, folder_id, name, path, true, (error, entity) ->
fs.unlink path, ->
timer.done()
@@ -50,6 +51,3 @@ module.exports = ProjectUploadController =
project_id: project_id, file_path: path, file_name: name, folder_id: folder_id
"uploaded file"
res.send success: true, entity_id: entity?._id
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
metrics = require("../../infrastructure/Metrics")
Url = require("url")
AuthenticationManager = require("../Authentication/AuthenticationManager")
AuthenticationController = require('../Authentication/AuthenticationController')
UserSessionsManager = require("./UserSessionsManager")
UserUpdater = require("./UserUpdater")
settings = require "settings-sharelatex"
@@ -15,20 +16,21 @@ settings = require "settings-sharelatex"
module.exports = UserController =
deleteUser: (req, res)->
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
UserDeleter.deleteUser user_id, (err)->
if !err?
req.session?.destroy()
res.sendStatus(200)
unsubscribe: (req, res)->
UserLocator.findById req.session.user._id, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
UserLocator.findById user_id, (err, user)->
newsLetterManager.unsubscribe user, ->
res.send()
updateUserSettings : (req, res)->
logger.log user: req.session.user, "updating account settings"
user_id = req.session.user._id
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "updating account settings"
User.findById user_id, (err, user)->
if err? or !user?
logger.err err:err, user_id:user_id, "problem updaing user settings"
@@ -54,9 +56,12 @@ module.exports = UserController =
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
if req.body.pdfViewer?
user.ace.pdfViewer = req.body.pdfViewer
if req.body.syntaxValidation?
user.ace.syntaxValidation = req.body.syntaxValidation
user.save (err)->
newEmail = req.body.email?.trim().toLowerCase()
if !newEmail? or newEmail == user.email
AuthenticationController.setInSessionUser(req, {first_name: user.first_name, last_name: user.last_name})
return res.sendStatus 200
else if newEmail.indexOf("@") == -1
return res.sendStatus(400)
@@ -73,7 +78,7 @@ module.exports = UserController =
if err?
logger.err err:err, user_id:user_id, "error getting user for email update"
return res.send 500
req.session.user.email = user.email
AuthenticationController.setInSessionUser(req, {email: user.email, first_name: user.first_name, last_name: user.last_name})
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
if err?
logger.err err:err, "error populateGroupLicenceInvite"
@@ -81,9 +86,10 @@ module.exports = UserController =
logout : (req, res)->
metrics.inc "user.logout"
logger.log user: req?.session?.user, "logging out"
user = AuthenticationController.getSessionUser(req)
logger.log user: user, "logging out"
sessionId = req.sessionID
user = req?.session?.user
req.logout?() # passport logout
req.session.destroy (err)->
if err
logger.err err: err, 'error destorying session'
@@ -102,13 +108,22 @@ module.exports = UserController =
setNewPasswordUrl: setNewPasswordUrl
}
clearSessions: (req, res, next = (error) ->) ->
metrics.inc "user.clear-sessions"
user = AuthenticationController.getSessionUser(req)
logger.log {user_id: user._id}, "clearing sessions for user"
UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
return next(err) if err?
res.sendStatus 201
changePassword : (req, res, next = (error) ->)->
metrics.inc "user.password-change"
oldPass = req.body.currentPassword
AuthenticationManager.authenticate {_id:req.session.user._id}, oldPass, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
AuthenticationManager.authenticate {_id:user_id}, oldPass, (err, user)->
return next(err) if err?
if(user)
logger.log user: req.session.user, "changing password"
logger.log user: user._id, "changing password"
newPassword1 = req.body.newPassword1
newPassword2 = req.body.newPassword2
if newPassword1 != newPassword2
@@ -3,12 +3,14 @@ logger = require("logger-sharelatex")
UserDeleter = require("./UserDeleter")
UserUpdater = require("./UserUpdater")
sanitize = require('sanitizer')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports = UserController =
getLoggedInUsersPersonalInfo: (req, res, next = (error) ->) ->
logger.log user: req.user, "reciving request for getting logged in users personal info"
return next(new Error("User is not logged in")) if !req.user?
UserGetter.getUser req.user._id, {
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user_id: user_id, "reciving request for getting logged in users personal info"
return next(new Error("User is not logged in")) if !user_id?
UserGetter.getUser user_id, {
first_name: true, last_name: true,
role:true, institution:true,
email: true, signUpDate: true
@@ -38,6 +40,3 @@ module.exports = UserController =
role: user.role
institution: user.institution
}
@@ -1,9 +1,11 @@
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
UserSessionsManager = require("./UserSessionsManager")
ErrorController = require("../Errors/ErrorController")
logger = require("logger-sharelatex")
Settings = require("settings-sharelatex")
fs = require('fs')
AuthenticationController = require('../Authentication/AuthenticationController')
module.exports =
@@ -22,14 +24,14 @@ module.exports =
sharedProjectData: sharedProjectData
newTemplateData: newTemplateData
new_email:req.query.new_email || ""
activateAccountPage: (req, res) ->
# An 'activation' is actually just a password reset on an account that
# was set with a random password originally.
logger.log query:req.query, "activiate account page called"
if !req.query?.user_id? or !req.query?.token?
return ErrorController.notFound(req, res)
UserGetter.getUser req.query.user_id, {email: 1, loginCount: 1}, (error, user) ->
return next(error) if error?
if !user
@@ -53,11 +55,23 @@ module.exports =
email: req.query.email
settingsPage : (req, res, next)->
logger.log user: req.session.user, "loading settings page"
UserLocator.findById req.session.user._id, (err, user)->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user: user_id, "loading settings page"
UserLocator.findById user_id, (err, user)->
return next(err) if err?
res.render 'user/settings',
title:'account_settings'
user: user,
languages: Settings.languages,
accountSettingsTabActive: true
sessionsPage: (req, res, next) ->
user = AuthenticationController.getSessionUser(req)
logger.log user_id: user._id, "loading sessions page"
UserSessionsManager.getAllUserSessions user, [req.sessionID], (err, sessions) ->
if err?
logger.err {user_id: user._id}, "error getting all user sessions"
return next(err)
res.render 'user/sessions',
title: "sessions"
sessions: sessions
@@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
crypto = require("crypto")
EmailHandler = require("../Email/EmailHandler")
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
Analytics = require "../Analytics/AnalyticsManager"
settings = require "settings-sharelatex"
module.exports = UserRegistrationHandler =
@@ -62,6 +63,7 @@ module.exports = UserRegistrationHandler =
cb() #this can be slow, just fire it off
], (err)->
logger.log user: user, "registered"
Analytics.recordEvent user._id, "user-registered"
callback(err, user)
registerNewUserAndSendActivationEmail: (email, callback = (error, user, setNewPasswordUrl) ->) ->
@@ -55,11 +55,40 @@ module.exports = UserSessionsManager =
UserSessionsManager._checkSessions(user, () ->)
callback()
getAllUserSessions: (user, exclude, callback=(err, sessionKeys)->) ->
exclude = _.map(exclude, UserSessionsManager._sessionKey)
sessionSetKey = UserSessionsManager._sessionSetKey(user)
rclient.smembers sessionSetKey, (err, sessionKeys) ->
if err?
logger.err user_id: user._id, "error getting all session keys for user from redis"
return callback(err)
sessionKeys = _.filter sessionKeys, (k) -> !(_.contains(exclude, k))
if sessionKeys.length == 0
logger.log {user_id: user._id}, "no other sessions found, returning"
return callback(null, [])
rclient.mget sessionKeys, (err, sessions) ->
if err?
logger.err {user_id: user._id}, "error getting all sessions for user from redis"
return callback(err)
result = []
for session in sessions
if session is null
continue
session = JSON.parse(session)
session_user = session?.user or session?.passport?.user
result.push {
ip_address: session_user.ip_address,
session_created: session_user.session_created
}
return callback(null, result)
revokeAllUserSessions: (user, retain, callback=(err)->) ->
if !retain
if !retain?
retain = []
retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
if !user
if !user?
logger.log {}, "no user to revoke sessions for, returning"
return callback(null)
logger.log {user_id: user._id}, "revoking all existing sessions for user"
@@ -5,42 +5,54 @@ Settings = require('settings-sharelatex')
SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatters')
querystring = require('querystring')
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
AuthenticationController = require("../Features/Authentication/AuthenticationController")
_ = require("underscore")
async = require("async")
Modules = require "./Modules"
Url = require "url"
PackageVersions = require "./PackageVersions"
fingerprints = {}
Path = require 'path'
jsPath =
if Settings.useMinifiedJs
"/minjs/"
else
"/js/"
ace = PackageVersions.lib('ace')
pdfjs = PackageVersions.lib('pdfjs')
logger.log "Generating file fingerprints..."
for path in [
"#{jsPath}libs/require.js",
"#{jsPath}ide.js",
"#{jsPath}main.js",
"#{jsPath}libs.js",
"#{jsPath}ace/ace.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.js",
"#{jsPath}libs/pdfjs-1.3.91/pdf.worker.js",
"#{jsPath}libs/pdfjs-1.3.91/compatibility.js",
"/stylesheets/style.css"
]
filePath = Path.join __dirname, "../../../", "public#{path}"
getFileContent = (filePath)->
filePath = Path.join __dirname, "../../../", "public#{filePath}"
exists = fs.existsSync filePath
if exists
content = fs.readFileSync filePath
hash = crypto.createHash("md5").update(content).digest("hex")
logger.log "#{filePath}: #{hash}"
fingerprints[path] = hash
return content
else
logger.log filePath:filePath, "file does not exist for fingerprints"
return ""
logger.log "Generating file fingerprints..."
pathList = [
["#{jsPath}libs/require.js"]
["#{jsPath}ide.js"]
["#{jsPath}main.js"]
["#{jsPath}libs.js"]
["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js", "#{jsPath}#{ace}/snippets/latex.js"]
["#{jsPath}libs/#{pdfjs}/pdf.js"]
["#{jsPath}libs/#{pdfjs}/pdf.worker.js"]
["#{jsPath}libs/#{pdfjs}/compatibility.js"]
["/stylesheets/style.css"]
]
for paths in pathList
contentList = _.map(paths, getFileContent)
content = contentList.join("")
hash = crypto.createHash("md5").update(content).digest("hex")
_.each paths, (filePath)->
logger.log "#{filePath}: #{hash}"
fingerprints[filePath] = hash
getFingerprint = (path) ->
if fingerprints[path]?
@@ -59,28 +71,35 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.session = req.session
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
user_id = AuthenticationController.getLoggedInUserId(req)
if cdnBlocked and !req.session.cdnBlocked?
logger.log user_id:user_id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
req.session.cdnBlocked = true
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
isLive = !isDark and !isSmoke
if cdnAvailable and isLive
if cdnAvailable and isLive and !cdnBlocked
staticFilesBase = Settings.cdn?.web?.host
else if darkCdnAvailable and isDark
staticFilesBase = Settings.cdn?.web?.darkHost
else
staticFilesBase = ""
res.locals.jsPath = jsPath
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
res.locals.lib = PackageVersions.lib
res.locals.buildJsPath = (jsFile, opts = {})->
path = Path.join(jsPath, jsFile)
doFingerPrint = opts.fingerprint != false
if !opts.qs?
opts.qs = {}
@@ -89,14 +108,13 @@ module.exports = (app, webRouter, apiRouter)->
if opts.cdn != false
path = Url.resolve(staticFilesBase, path)
qs = querystring.stringify(opts.qs)
if qs? and qs.length > 0
path = path + "?" + qs
return path
res.locals.buildCssPath = (cssFile)->
path = Path.join("/stylesheets/", cssFile)
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
@@ -109,7 +127,7 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.settings = Settings
next()
@@ -117,7 +135,9 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.translate = (key, vars = {}) ->
vars.appName = Settings.appName
req.i18n.translate(key, vars)
res.locals.currentUrl = req.originalUrl
# Don't include the query string parameters, otherwise Google
# treats ?nocdn=true as the canonical version
res.locals.currentUrl = Url.parse(req.originalUrl).pathname
next()
webRouter.use (req, res, next)->
@@ -125,9 +145,10 @@ module.exports = (app, webRouter, apiRouter)->
Settings.siteUrl.substring(Settings.siteUrl.indexOf("//")+2)
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next) ->
res.locals.getUserEmail = ->
email = req?.session?.user?.email or ""
user = AuthenticationController.getSessionUser(req)
email = user?.email or ""
return email
next()
@@ -137,15 +158,17 @@ module.exports = (app, webRouter, apiRouter)->
return formatedPrivileges[privilegeLevel] || "Private"
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.buildReferalUrl = (referal_medium) ->
url = Settings.siteUrl
if req.session? and req.session.user? and req.session.user.referal_id?
url+="?r=#{req.session.user.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
currentUser = AuthenticationController.getSessionUser(req)
if currentUser? and currentUser?.referal_id?
url+="?r=#{currentUser.referal_id}&rm=#{referal_medium}&rs=b" # Referal source = bonus
return url
res.locals.getReferalId = ->
if req.session? and req.session.user? and req.session.user.referal_id
return req.session.user.referal_id
currentUser = AuthenticationController.getSessionUser(req)
if currentUser? and currentUser?.referal_id?
return currentUser.referal_id
res.locals.getReferalTagLine = ->
tagLines = [
"Roar!"
@@ -161,7 +184,11 @@ module.exports = (app, webRouter, apiRouter)->
return ""
res.locals.getLoggedInUserId = ->
return req.session.user?._id
return AuthenticationController.getLoggedInUserId(req)
res.locals.isUserLoggedIn = ->
return AuthenticationController.isUserLoggedIn(req)
res.locals.getSessionUser = ->
return AuthenticationController.getSessionUser(req)
next()
webRouter.use (req, res, next) ->
@@ -173,11 +200,11 @@ module.exports = (app, webRouter, apiRouter)->
return req.query?[field]
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.fingerprint = getFingerprint
next()
webRouter.use (req, res, next)->
webRouter.use (req, res, next)->
res.locals.formatPrice = SubscriptionFormatters.formatPrice
next()
@@ -187,11 +214,12 @@ module.exports = (app, webRouter, apiRouter)->
next()
webRouter.use (req, res, next)->
if req.session.user?
currentUser = AuthenticationController.getSessionUser(req)
if currentUser?
res.locals.user =
email: req.session.user.email
first_name: req.session.user.first_name
last_name: req.session.user.last_name
email: currentUser.email
first_name: currentUser.first_name
last_name: currentUser.last_name
if req.session.justRegistered
res.locals.justRegistered = true
delete req.session.justRegistered
@@ -217,7 +245,7 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.nav[key] = _.clone(Settings.nav[key])
res.locals.templates = Settings.templateLinks
next()
webRouter.use (req, res, next) ->
SystemMessageManager.getMessages (error, messages = []) ->
res.locals.systemMessages = messages
@@ -240,5 +268,3 @@ module.exports = (app, webRouter, apiRouter)->
res.locals.moduleIncludes = Modules.moduleIncludes
res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
next()
@@ -2,6 +2,7 @@ request = require("request")
settings = require("settings-sharelatex")
_ = require("underscore")
logger = require("logger-sharelatex")
URL = require("url")
currencyMappings = {
"GB":"GBP"
@@ -31,7 +32,7 @@ module.exports = GeoIpLookup =
return callback(e)
ip = ip.trim().split(" ")[0]
opts =
url: "#{settings.apis.geoIpLookup.url}/#{ip}"
url: URL.resolve(settings.apis.geoIpLookup.url,ip)
timeout: 1000
json:true
logger.log ip:ip, opts:opts, "getting geo ip details"
@@ -25,14 +25,14 @@ module.exports = Modules =
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
@viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade"))
@viewIncludes[view].push jade.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade")), doctype: "html")
moduleIncludes: (view, locals) ->
partials = Modules.viewIncludes[view] or []
compiledPartials = Modules.viewIncludes[view] or []
html = ""
for partial in partials
compiler = jade.compile(partial, doctype: "html")
html += compiler(locals)
for compiledPartial in compiledPartials
d = new Date()
html += compiledPartial(locals)
return html
moduleIncludesAvailable: (view) ->
@@ -0,0 +1,15 @@
version = {
"pdfjs": "1.6.210p1"
"moment": "2.9.0"
"ace": "1.2.5"
}
module.exports = {
version: version
lib: (name) ->
if version[name]?
return "#{name}-#{version[name]}"
else
return "#{name}"
}
@@ -21,6 +21,9 @@ cookieParser = require('cookie-parser')
sessionStore = new RedisStore(client:rclient)
passport = require('passport')
LocalStrategy = require('passport-local').Strategy
Mongoose = require("./Mongoose")
oneDayInMilliseconds = 86400000
@@ -32,6 +35,7 @@ Modules = require "./Modules"
ErrorController = require "../Features/Errors/ErrorController"
UserSessionsManager = require "../Features/User/UserSessionsManager"
AuthenticationController = require "../Features/Authentication/AuthenticationController"
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
@@ -87,11 +91,26 @@ webRouter.use csrfProtection
webRouter.use translations.expressMiddlewear
webRouter.use translations.setLangBasedOnDomainMiddlewear
# passport
webRouter.use passport.initialize()
webRouter.use passport.session()
passport.use(new LocalStrategy(
{
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password'
},
AuthenticationController.doPassportLogin
))
passport.serializeUser(AuthenticationController.serializeUser)
passport.deserializeUser(AuthenticationController.deserializeUser)
# Measure expiry from last request, not last login
webRouter.use (req, res, next) ->
req.session.touch()
if req?.session?.user?
UserSessionsManager.touch(req.session.user, (err)->)
if AuthenticationController.isUserLoggedIn(req)
UserSessionsManager.touch(AuthenticationController.getSessionUser(req), (err)->)
next()
webRouter.use ReferalConnect.use
@@ -1,23 +1,42 @@
mongoose = require 'mongoose'
Settings = require 'settings-sharelatex'
Schema = mongoose.Schema
ObjectId = Schema.ObjectId
ProjectInviteSchema = new Schema
project_id: ObjectId
from_user_id: ObjectId
privilegeLevel: String
# For existing users
to_user_id: ObjectId
# For non-existant users
hashed_token: String
email: String
EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30
ExpiryDate = () ->
timestamp = new Date()
timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS)
return timestamp
ProjectInviteSchema = new Schema(
{
email: String
token: String
sendingUserId: ObjectId
projectId: ObjectId
privileges: String
createdAt: {type: Date, default: Date.now}
expires: {type: Date, default: ExpiryDate, index: {expireAfterSeconds: 10}}
},
{
collection: 'projectInvites'
}
)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
ProjectInvite = conn.model('ProjectInvite', ProjectInviteSchema)
mongoose.model 'ProjectInvite', ProjectInviteSchema
exports.ProjectInvite = ProjectInvite
exports.ProjectInviteSchema = ProjectInviteSchema
exports.ProjectInviteSchema = ProjectInviteSchema
exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS
@@ -26,6 +26,7 @@ UserSchema = new Schema
autoComplete: {type : Boolean, default: true}
spellCheckLanguage : {type : String, default: "en"}
pdfViewer : {type : String, default: "pdfjs"}
syntaxValidation : {type : Boolean, default: true}
}
features : {
collaborators: { type:Number, default: Settings.defaultFeatures.collaborators }
+29 -10
View File
@@ -11,7 +11,6 @@ SubscriptionRouter = require './Features/Subscription/SubscriptionRouter'
UploadsRouter = require './Features/Uploads/UploadsRouter'
metrics = require('./infrastructure/Metrics')
ReferalController = require('./Features/Referal/ReferalController')
ReferalMiddleware = require('./Features/Referal/ReferalMiddleware')
AuthenticationController = require('./Features/Authentication/AuthenticationController')
TagsController = require("./Features/Tags/TagsController")
NotificationsController = require("./Features/Notifications/NotificationsController")
@@ -22,6 +21,7 @@ UserPagesController = require('./Features/User/UserPagesController')
DocumentController = require('./Features/Documents/DocumentController')
CompileManager = require("./Features/Compile/CompileManager")
CompileController = require("./Features/Compile/CompileController")
ClsiCookieManager = require("./Features/Compile/ClsiCookieManager")
HealthCheckController = require("./Features/HealthCheck/HealthCheckController")
ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController"
FileStoreController = require("./Features/FileStore/FileStoreController")
@@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter")
ReferencesController = require('./Features/References/ReferencesController')
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
logger = require("logger-sharelatex")
_ = require("underscore")
@@ -51,7 +52,8 @@ module.exports = class Router
webRouter.get '/login', UserPagesController.loginPage
AuthenticationController.addEndpointToLoginWhitelist '/login'
webRouter.post '/login', AuthenticationController.login
webRouter.post '/login', AuthenticationController.passportLogin
webRouter.get '/logout', UserController.logout
webRouter.get '/restricted', AuthorizationMiddlewear.restricted
@@ -68,12 +70,13 @@ module.exports = class Router
StaticPagesRouter.apply(webRouter, apiRouter)
RealTimeProxyRouter.apply(webRouter, apiRouter)
ContactRouter.apply(webRouter, apiRouter)
AnalyticsRouter.apply(webRouter, apiRouter)
Modules.applyRouter(webRouter, apiRouter)
if Settings.enableSubscriptions
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalMiddleware.getUserReferalId, ReferalController.bonus
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
webRouter.get '/blog', BlogController.getIndexPage
webRouter.get '/blog/*', BlogController.getPage
@@ -85,6 +88,9 @@ module.exports = class Router
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
webRouter.post '/user/password/update', AuthenticationController.requireLogin(), UserController.changePassword
webRouter.get '/user/sessions', AuthenticationController.requireLogin(), UserPagesController.sessionsPage
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
@@ -179,7 +185,7 @@ module.exports = class Router
webRouter.delete '/tag/:tag_id/project/:project_id', AuthenticationController.requireLogin(), TagsController.removeProjectFromTag
webRouter.get '/notifications', AuthenticationController.requireLogin(), NotificationsController.getAllUnreadNotifications
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
webRouter.delete '/notifications/:notification_id', AuthenticationController.requireLogin(), NotificationsController.markNotificationAsRead
# Deprecated in favour of /internal/project/:project_id but still used by versioning
apiRouter.get '/project/:project_id/details', AuthenticationController.httpAuth, ProjectApiController.getProjectDetails
@@ -249,14 +255,27 @@ module.exports = class Router
apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
project_id = req.params.Project_id
sendRes = _.once (statusCode, message)->
res.writeHead statusCode
res.end message
CompileManager.compile req.params.Project_id, "test-compile", {}, () ->
sendRes 200, "Compiler returned in less than 10 seconds"
setTimeout (() ->
res.status statusCode
res.send message
ClsiCookieManager.clearServerId project_id # force every compile to a new server
# set a timeout
handler = setTimeout (() ->
sendRes 500, "Compiler timed out"
handler = null
), 10000
# use a valid user id for testing
test_user_id = "123456789012345678901234"
# run the compile
CompileManager.compile project_id, test_user_id, {}, (error, status) ->
clearTimeout handler if handler?
if error?
sendRes 500, "Compiler returned error #{error.message}"
else if status is "success"
sendRes 200, "Compiler returned in less than 10 seconds"
else
sendRes 500, "Compiler returned failure #{status}"
apiRouter.get "/ip", (req, res, next) ->
res.send({
@@ -280,4 +299,4 @@ module.exports = class Router
metrics.inc("client-side-error")
res.sendStatus(204)
webRouter.get '*', ErrorController.notFound
webRouter.get '*', ErrorController.notFound
@@ -22,7 +22,7 @@ block content
ul.list-unstyled.text-center
li
i.fa.fa-fw.fa-book
| &nbsp;#{translate("mendeley_integration")}
| &nbsp;#{translate("syntax_checking")}
.row.text-centered
.col-md-12
if user.betaProgram
+20 -77
View File
@@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product')
window.similarproducts = true
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
-if (typeof(gaExperiments) != "undefined")
|!{gaExperiments}
@@ -46,83 +47,22 @@ html(itemscope, itemtype='http://schema.org/Product')
- else
script(type='text/javascript').
window.ga = function() { console.log("would send to GA", arguments) };
// Countly Analytics
if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
script(type="text/javascript").
var Countly = Countly || {};
Countly.q = Countly.q || [];
Countly.app_key = '#{settings.analytics.countly.token}';
Countly.url = '#{settings.analytics.countly.server}';
!{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' }
(function() {
var cly = document.createElement('script'); cly.type = 'text/javascript';
cly.async = true;
//enter url of script here
cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
cly.onload = function(){Countly.init()};
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
})();
script(type="text/javascript")
if (session && session.user)
- var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
| Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
if (justRegistered)
| Countly.q.push(['add_event',{ key: 'user-registered' }]);
if (justLoggedIn)
| Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
if (user && user.features)
- featureFlagSet = false;
if user.features.hasOwnProperty('collaborators')
| Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('compileGroup')
| Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('compileTimeout')
| Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('dropbox')
| Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('github')
| Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('references')
| Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('templates')
| Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
- featureFlagSet = true;
if user.features.hasOwnProperty('versioning')
| Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
- featureFlagSet = true;
if featureFlagSet
| Countly.q.push(['userData.save'])
// End countly Analytics
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
block scripts
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
window.location.search += '&'+noCdnKey;
}
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script.
window.sharelatex = {
siteUrl: '#{settings.siteUrl}',
@@ -171,23 +111,27 @@ html(itemscope, itemtype='http://schema.org/Product')
include layout/navbar
block content
div(ng-controller="AbTestController")
- if(typeof(suppressFooter) == "undefined")
include layout/footer
- if (typeof(lookingForScribtex) != "undefined" && lookingForScribtex)
span(ng-controller="ScribtexPopupController")
include scribtex-modal
- if(typeof(suppressFooter) == "undefined")
block requirejs
script(type='text/javascript').
// minimal requirejs configuration (can be extended/overridden)
window.requirejs = {
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"paths" : {
"moment": "libs/moment-2.7.0"
"moment": "libs/#{lib('moment')}"
},
"urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}",
"config":{
"moment":{
"noGlobal": true
}
}
};
script(
@@ -196,7 +140,6 @@ html(itemscope, itemtype='http://schema.org/Product')
src=buildJsPath('libs/require.js')
)
include contact-us-modal
include sentry
+2 -2
View File
@@ -13,7 +13,7 @@ nav.navbar.navbar-default
.navbar-collapse.collapse(collapse="navCollapsed")
ul.nav.navbar-nav.navbar-right
if (session && session.user && session.user.isAdmin)
if (getSessionUser() && getSessionUser().isAdmin)
li.dropdown(class="subdued", dropdown)
a.dropdown-toggle(href, dropdown-toggle)
| Admin
@@ -25,7 +25,7 @@ nav.navbar.navbar-default
a(href="/admin/user") Manage Users
each item in nav.header
if ((item.only_when_logged_in && session && session.user) || (item.only_when_logged_out && (!session || !session.user)) || (!item.only_when_logged_out && !item.only_when_logged_in))
if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
if item.dropdown
li.dropdown(class=item.class, dropdown)
a.dropdown-toggle(href, dropdown-toggle)
+23 -24
View File
@@ -3,7 +3,6 @@ extends ../layout
block vars
- var suppressNavbar = true
- var suppressFooter = true
- var suppressDefaultJs = true
- var suppressSystemMessages = true
block content
@@ -66,7 +65,7 @@ block content
.ui-layout-center
include ./editor/editor
include ./editor/binary-file
include ./editor/track-changes
include ./editor/history
include ./editor/publish-template
.ui-layout-east
@@ -86,7 +85,11 @@ block content
.modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")}
script(src='/socket.io/socket.io.js')
block requirejs
script(type="text/javascript" src='/socket.io/socket.io.js')
//- don't use cdn for worker
- var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false})
//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
//- and doesn't prematurely end the script tag.
@@ -100,47 +103,43 @@ block content
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
window.requirejs = {
"paths" : {
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
"moment": "libs/moment-2.7.0",
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}",
"moment": "libs/#{lib('moment')}",
"pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf",
"pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}",
"ace": "#{lib('ace')}"
},
"urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}",
"waitSeconds": 0,
"shim": {
"libs/pdf": {
deps: ["libs/pdfjs-1.3.91/compatibility"]
"pdfjs-dist/build/pdf": {
"deps": ["libs/#{lib('pdfjs')}/compatibility"]
},
"ace/ext-searchbox": {
deps: ["ace/ace"]
"deps": ["ace/ace"]
},
"ace/ext-language_tools": {
deps: ["ace/ace"]
"deps": ["ace/ace"]
}
},
config:{
moment:{
noGlobal: true
"config":{
"moment":{
"noGlobal": true
}
}
};
window.aceFingerprint = "#{fingerprint(jsPath + 'ace/ace.js')}"
- locals.suppressDefaultJs = true
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
- var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn
window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
- var aceWorkerPath = user.betaProgram ? buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) : "" // don't use cdn for worker
script(type='text/javascript').
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
window.aceWorkerPath = "#{aceWorkerPath}";
script(
data-main=buildJsPath("ide.js", {fingerprint:false}),
baseurl=fullJsPath,
data-ace-base=buildJsPath('ace', {fingerprint:false}),
data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}),
src=buildJsPath('libs/require.js')
)
@@ -41,11 +41,12 @@ aside.chat(
}"
)
.arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}")
p(
mathjax,
ng-repeat="content in message.contents track by $index"
)
span(ng-bind-html="content | linky:'_blank'")
.message-content
p(
mathjax,
ng-repeat="content in message.contents track by $index"
)
span(ng-bind-html="content | linky:'_blank'")
.new-message
textarea(
@@ -36,6 +36,7 @@ div.full-size(
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
read-only="!permissions.write",
on-ctrl-enter="recompileViaKey"
syntax-validation="settings.syntaxValidation"
)
.ui-layout-east
@@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']"
)
li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'")
li(ng-show="deletedDocs.length > 0 && ui.view == 'history'")
h3 #{translate("deleted_files")}
li(
ng-class="{ 'selected': entity.selected }",
ng-repeat="entity in deletedDocs | orderBy:'name'",
ng-controller="FileTreeEntityController",
ng-show="ui.view == 'track-changes'"
ng-show="ui.view == 'history'"
)
.entity
.entity-name(
@@ -315,8 +315,12 @@ script(type='text/ng-template', id='newDocModalTemplate')
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open"
select-name-on="open",
ng-pattern="validFileRegex",
name="name"
)
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.pattern")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
@@ -341,8 +345,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open"
select-name-on="open",
ng-pattern="validFileRegex",
name="name"
)
.text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.pattern")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
@@ -414,3 +422,13 @@ script(type='text/ng-template', id='deleteEntityModalTemplate')
)
span(ng-hide="state.inflight") #{translate("delete")}
span(ng-show="state.inflight") #{translate("deleting")}...
script(type='text/ng-template', id='invalidFileNameModalTemplate')
.modal-header
h3 #{translate('invalid_file_name')}
.modal-body
p #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-click="$close()"
) #{translate('ok')}
+375 -115
View File
@@ -1,120 +1,380 @@
header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading")
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true"
tooltip='#{translate("menu")}',
tooltip-placement="bottom",
tooltip-append-to-body="true"
div(ng-if="!shouldABTestHeaderLabels")
header.toolbar.toolbar-header(
ng-cloak,
ng-hide="state.loading"
)
i.fa.fa-fw.fa-bars
a(
href="/project"
tooltip="#{translate('back_to_projects')}",
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
i.fa.fa-fw.fa-level-up
span(ng-controller="PdfViewToggleController")
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true"
tooltip='#{translate("menu")}',
tooltip-placement="bottom",
tooltip-append-to-body="true",
)
i.fa.fa-fw.fa-bars
a(
href,
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
tooltip="PDF",
href="/project"
tooltip="#{translate('back_to_projects')}",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="togglePdfView()",
ng-class="{ 'active': ui.view == 'pdf' }"
tooltip-append-to-body="true"
)
i.fa.fa-file-pdf-o
.toolbar-center.project-name(ng-controller="ProjectNameController")
span.name(
ng-dblclick="!permissions.admin || startRenaming()",
ng-show="!state.renaming"
) {{ project.name }}
input.form-control(
type="text"
ng-model="inputs.name",
ng-show="state.renaming",
on-enter="finishRenaming()",
ng-blur="finishRenaming()",
select-name-when="state.renaming"
)
a.rename(
ng-if="permissions.admin",
href='#',
tooltip-placement="bottom",
tooltip="#{translate('rename')}",
tooltip-append-to-body="true",
ng-click="startRenaming()",
ng-show="!state.renaming"
)
i.fa.fa-pencil
.toolbar-right
span.online-users(
ng-show="onlineUsersArray.length > 0"
ng-controller="OnlineUsersController"
)
span(ng-if="onlineUsersArray.length < 4")
span.online-user(
ng-repeat="user in onlineUsersArray",
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
popover="{{ user.name }}"
popover-placement="bottom"
popover-append-to-body="true"
popover-trigger="mouseenter"
ng-click="gotoUser(user)"
) {{ user.name.slice(0,1) }}
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi(
dropdown-toggle,
tooltip="#{translate('connected_users')}",
tooltip-placement="left"
)
strong {{ onlineUsersArray.length }}
i.fa.fa-fw.fa-user
ul.dropdown-menu.pull-right
li.dropdown-header #{translate('connected_users')}
li(ng-repeat="user in onlineUsersArray")
a(href, ng-click="gotoUser(user)")
span.online-user(
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
) {{ user.name.slice(0,1) }}
| {{ user.name }}
a.btn.btn-full-height(
href,
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
ng-click="openShareProjectModal()",
ng-controller="ShareController"
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,
ng-click="toggleTrackChanges()",
ng-class="{ active: (ui.view == 'track-changes') }"
tooltip="#{translate('recent_changes')}",
tooltip-placement="bottom"
)
i.fa.fa-fw.fa-history
a.btn.btn-full-height(
href,
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
ng-click="toggleChat()",
ng-controller="ChatButtonController",
ng-show="!anonymous"
)
i.fa.fa-fw.fa-comment(
ng-class="{ 'bounce': unreadMessages > 0 }"
i.fa.fa-fw.fa-level-up
span(ng-controller="PdfViewToggleController")
a(
href,
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
tooltip="PDF",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="togglePdfView()",
ng-class="{ 'active': ui.view == 'pdf' }"
)
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
i.fa.fa-file-pdf-o
.toolbar-center.project-name(ng-controller="ProjectNameController")
span.name(
ng-dblclick="!permissions.admin || startRenaming()",
ng-show="!state.renaming"
) {{ project.name }}
input.form-control(
type="text"
ng-model="inputs.name",
ng-show="state.renaming",
on-enter="finishRenaming()",
ng-blur="finishRenaming()",
select-name-when="state.renaming"
)
a.rename(
ng-if="permissions.admin",
href='#',
tooltip-placement="bottom",
tooltip="#{translate('rename')}",
tooltip-append-to-body="true",
ng-click="startRenaming()",
ng-show="!state.renaming"
)
i.fa.fa-pencil
.toolbar-right
span.online-users(
ng-show="onlineUsersArray.length > 0"
ng-controller="OnlineUsersController"
)
span(ng-if="onlineUsersArray.length < 4")
span.online-user(
ng-repeat="user in onlineUsersArray",
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
popover="{{ user.name }}"
popover-placement="bottom"
popover-append-to-body="true"
popover-trigger="mouseenter"
ng-click="gotoUser(user)"
) {{ user.name.slice(0,1) }}
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi(
dropdown-toggle,
tooltip="#{translate('connected_users')}",
tooltip-placement="left"
)
strong {{ onlineUsersArray.length }}
i.fa.fa-fw.fa-user
ul.dropdown-menu.pull-right
li.dropdown-header #{translate('connected_users')}
li(ng-repeat="user in onlineUsersArray")
a(href, ng-click="gotoUser(user)")
span.online-user(
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
) {{ user.name.slice(0,1) }}
| {{ user.name }}
a.btn.btn-full-height(
href,
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
ng-click="openShareProjectModal()",
ng-controller="ShareController",
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,
ng-click="toggleHistory()",
ng-class="{ active: (ui.view == 'history') }"
tooltip="#{translate('recent_changes')}",
tooltip-placement="bottom",
)
i.fa.fa-fw.fa-history
a.btn.btn-full-height(
href,
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
ng-click="toggleChat()",
ng-controller="ChatButtonController",
ng-show="!anonymous",
)
i.fa.fa-fw.fa-comment(
ng-class="{ 'bounce': unreadMessages > 0 }"
)
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
div(ng-if="shouldABTestHeaderLabels")
div(sixpack-switch="editor-header")
header.toolbar.toolbar-header(
ng-cloak,
ng-hide="state.loading"
sixpack-default
)
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');"
tooltip='#{translate("menu")}',
tooltip-placement="bottom",
tooltip-append-to-body="true",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-bars
a(
href="/project"
tooltip="#{translate('back_to_projects')}",
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
i.fa.fa-fw.fa-level-up
span(ng-controller="PdfViewToggleController")
a(
href,
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
tooltip="PDF",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="togglePdfView()",
ng-class="{ 'active': ui.view == 'pdf' }"
)
i.fa.fa-file-pdf-o
.toolbar-center.project-name(ng-controller="ProjectNameController")
span.name(
ng-dblclick="!permissions.admin || startRenaming()",
ng-show="!state.renaming"
) {{ project.name }}
input.form-control(
type="text"
ng-model="inputs.name",
ng-show="state.renaming",
on-enter="finishRenaming()",
ng-blur="finishRenaming()",
select-name-when="state.renaming"
)
a.rename(
ng-if="permissions.admin",
href='#',
tooltip-placement="bottom",
tooltip="#{translate('rename')}",
tooltip-append-to-body="true",
ng-click="startRenaming()",
ng-show="!state.renaming"
)
i.fa.fa-pencil
.toolbar-right
span.online-users(
ng-show="onlineUsersArray.length > 0"
ng-controller="OnlineUsersController"
)
span(ng-if="onlineUsersArray.length < 4")
span.online-user(
ng-repeat="user in onlineUsersArray",
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
popover="{{ user.name }}"
popover-placement="bottom"
popover-append-to-body="true"
popover-trigger="mouseenter"
ng-click="gotoUser(user)"
) {{ user.name.slice(0,1) }}
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi(
dropdown-toggle,
tooltip="#{translate('connected_users')}",
tooltip-placement="left"
)
strong {{ onlineUsersArray.length }}
i.fa.fa-fw.fa-user
ul.dropdown-menu.pull-right
li.dropdown-header #{translate('connected_users')}
li(ng-repeat="user in onlineUsersArray")
a(href, ng-click="gotoUser(user)")
span.online-user(
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
) {{ user.name.slice(0,1) }}
| {{ user.name }}
a.btn.btn-full-height(
href,
ng-if="permissions.admin",
tooltip="#{translate('share')}",
tooltip-placement="bottom",
ng-click="openShareProjectModal(); trackABTestConversion('share');",
ng-controller="ShareController",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-group
a.btn.btn-full-height(
href,
ng-click="toggleHistory(); trackABTestConversion('history');",
ng-class="{ active: (ui.view == 'history') }"
tooltip="#{translate('recent_changes')}",
tooltip-placement="bottom",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-history
a.btn.btn-full-height(
href,
tooltip="#{translate('chat')}",
tooltip-placement="bottom",
ng-class="{ active: ui.chatOpen }",
ng-click="toggleChat(); trackABTestConversion('chat');",
ng-controller="ChatButtonController",
ng-show="!anonymous",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-comment(
ng-class="{ 'bounce': unreadMessages > 0 }"
)
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
header.toolbar.toolbar-header.toolbar-with-labels(
ng-cloak,
ng-hide="state.loading"
sixpack-when="labels"
)
.toolbar-left
a.btn.btn-full-height(
href,
ng-click="ui.leftMenuShown = true; trackABTestConversion('menu');",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-bars
p.toolbar-label #{translate("menu")}
a(
href="/project"
)
i.fa.fa-fw.fa-level-up
span(ng-controller="PdfViewToggleController")
a(
href,
ng-show="ui.pdfLayout == 'flat' && fileTreeClosed",
tooltip="PDF",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="togglePdfView()",
ng-class="{ 'active': ui.view == 'pdf' }"
)
i.fa.fa-file-pdf-o
.toolbar-center.project-name(ng-controller="ProjectNameController")
span.name(
ng-dblclick="!permissions.admin || startRenaming()",
ng-show="!state.renaming"
) {{ project.name }}
input.form-control(
type="text"
ng-model="inputs.name",
ng-show="state.renaming",
on-enter="finishRenaming()",
ng-blur="finishRenaming()",
select-name-when="state.renaming"
)
a.rename(
ng-if="permissions.admin",
href='#',
tooltip-placement="bottom",
tooltip="#{translate('rename')}",
tooltip-append-to-body="true",
ng-click="startRenaming()",
ng-show="!state.renaming"
)
i.fa.fa-pencil
.toolbar-right
span.online-users(
ng-show="onlineUsersArray.length > 0"
ng-controller="OnlineUsersController"
)
span(ng-if="onlineUsersArray.length < 4")
span.online-user(
ng-repeat="user in onlineUsersArray",
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }",
popover="{{ user.name }}"
popover-placement="bottom"
popover-append-to-body="true"
popover-trigger="mouseenter"
ng-click="gotoUser(user)"
) {{ user.name.slice(0,1) }}
span.dropdown(dropdown, ng-if="onlineUsersArray.length >= 4")
span.online-user.online-user-multi(
dropdown-toggle,
tooltip="#{translate('connected_users')}",
tooltip-placement="left"
)
strong {{ onlineUsersArray.length }}
i.fa.fa-fw.fa-user
ul.dropdown-menu.pull-right
li.dropdown-header #{translate('connected_users')}
li(ng-repeat="user in onlineUsersArray")
a(href, ng-click="gotoUser(user)")
span.online-user(
ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }"
) {{ user.name.slice(0,1) }}
| {{ user.name }}
a.btn.btn-full-height(
href,
ng-if="permissions.admin",
ng-click="openShareProjectModal(); trackABTestConversion('share');",
ng-controller="ShareController",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-group
p.toolbar-label #{translate("share")}
a.btn.btn-full-height(
href,
ng-click="toggleHistory(); trackABTestConversion('history');",
ng-class="{ active: (ui.view == 'history') }",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-history
p.toolbar-label #{translate("history")}
a.btn.btn-full-height(
href,
ng-class="{ active: ui.chatOpen }",
ng-click="toggleChat(); trackABTestConversion('chat');",
ng-controller="ChatButtonController",
ng-show="!anonymous",
sixpack-convert="editor-header"
)
i.fa.fa-fw.fa-comment(
ng-class="{ 'bounce': unreadMessages > 0 }"
)
span.label.label-info(
ng-show="unreadMessages > 0"
) {{ unreadMessages }}
p.toolbar-label #{translate("chat")}
@@ -1,5 +1,5 @@
div#trackChanges(ng-show="ui.view == 'track-changes'")
span(ng-controller="TrackChangesPremiumPopup")
div#history(ng-show="ui.view == 'history'")
span(ng-controller="HistoryPremiumPopup")
.upgrade-prompt(ng-show="!project.features.versioning")
.message(ng-show="project.owner._id == user.id")
p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
@@ -33,29 +33,29 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
a.btn.btn-success(
href
ng-class="buttonClass"
ng-click="startFreeTrial('track-changes')"
ng-click="startFreeTrial('history')"
) #{translate("start_free_trial")}
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
p
a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")}
a.small(href, ng-click="toggleHistory()") #{translate("cancel")}
aside.change-list(
ng-controller="TrackChangesListController"
ng-controller="HistoryListController"
infinite-scroll="loadMore()"
infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd"
infinite-scroll-initialize="ui.view == 'track-changes'"
infinite-scroll-disabled="history.loading || history.atEnd"
infinite-scroll-initialize="ui.view == 'history'"
)
.infinite-scroll-inner
ul.list-unstyled(
ng-class="{\
'hover-state': trackChanges.hoveringOverListSelectors\
'hover-state': history.hoveringOverListSelectors\
}"
)
li.change(
ng-repeat="update in trackChanges.updates"
ng-repeat="update in history.updates"
ng-class="{\
'first-in-day': update.meta.first_in_day,\
'selected': update.inSelection,\
@@ -65,7 +65,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
'hover-selected-to': update.hoverSelectedTo,\
'hover-selected-from': update.hoverSelectedFrom,\
}"
ng-controller="TrackChangesListItemController"
ng-controller="HistoryListItemController"
)
div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }}
@@ -108,57 +108,69 @@ div#trackChanges(ng-show="ui.view == 'track-changes'")
.color-square(style="background-color: hsl(100, 100%, 50%)")
span #{translate("anonymous")}
.loading(ng-show="trackChanges.loading")
.loading(ng-show="history.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp; #{translate("loading")}...
.diff-panel.full-size(ng-controller="TrackChangesDiffController")
.diff-panel.full-size(ng-controller="HistoryDiffController")
.diff(
ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error"
ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error"
)
.toolbar.toolbar-alt
span.name
| <strong>{{trackChanges.diff.highlights.length}} </strong>
| <strong>{{history.diff.highlights.length}} </strong>
ng-pluralize(
count="trackChanges.diff.highlights.length",
count="history.diff.highlights.length",
when="{\
'one': 'change',\
'other': 'changes'\
}"
)
| in <strong>{{trackChanges.diff.doc.name}}</strong>
| in <strong>{{history.diff.doc.name}}</strong>
.toolbar-right
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"
) #{translate("restore_to_before_these_changes")}
.diff-editor.hide-ace-cursor(
ace-editor="track-changes",
ace-editor="history",
theme="settings.theme",
font-size="settings.fontSize",
text="trackChanges.diff.text",
highlights="trackChanges.diff.highlights",
text="history.diff.text",
highlights="history.diff.highlights",
read-only="true",
resize-on="layout:main:resize",
navigate-highlights="true"
)
.diff-deleted.text-centered(
ng-show="trackChanges.diff.deleted"
ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})}
p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})}
p
a.btn.btn-primary.btn-lg(
href,
ng-click="restoreDeletedDoc()"
ng-click="restoreDeletedDoc()",
ng-disabled="history.diff.restoreInProgress"
) #{translate("restore")}
.loading-panel(ng-show="trackChanges.diff.loading")
.diff-deleted.text-centered(
ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess"
)
p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})}
p.text-serif #{translate("file_restored_back_to_editor")}
p
a.btn.btn-default(
href,
ng-click="backToEditorAfterRestore()",
) #{translate("file_restored_back_to_editor_btn")}
.loading-panel(ng-show="history.diff.loading")
i.fa.fa-spin.fa-refresh
| &nbsp;&nbsp;#{translate("loading")}...
.error-panel(ng-show="trackChanges.diff.error")
.error-panel(ng-show="history.diff.error")
.alert.alert-danger #{translate("generic_something_went_wrong")}
script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate")
script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
.modal-header
button.close(
type="button"
@@ -66,6 +66,31 @@ script(type="text/ng-template", id="hotkeysModalTemplate")
.hotkey
span.combination {{ctrl}} + I
span.description Italic Text
h3 #{translate("autocomplete")}
.row
.col-xs-6
.hotkey
span.combination Ctrl + Space
span.description Autocomplete Menu
.col-xs-6
.hotkey
span.combination Tab / Up / Down
span.description Select Candidate
.hotkey
span.combination Enter
span.description Insert Candidate
h3 !{translate("autocomplete_references")}
.row
.col-xs-6
.hotkey
span.combination Ctrl + Space
span.description Search References
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
@@ -105,6 +105,15 @@ aside#left-menu.full-size(
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
)
if (user.betaProgram)
.form-controls
label(for="syntaxValidation") #{translate("syntax_validation")}
select(
name="syntaxValidation"
ng-model="settings.syntaxValidation"
ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]"
)
.form-controls
label(for="theme") #{translate("theme")}
select(
@@ -197,6 +206,10 @@ script(type='text/ng-template', id='wordCountModalTemplate')
)
div(ng-if="!status.loading")
.container-fluid
.row(ng-show='data.messages.length > 0')
.col-xs-12
.alert.alert-danger
p(style="white-space: pre-wrap") {{data.messages}}
.row
.col-xs-4
.pull-right #{translate("total_words")} :
+36 -5
View File
@@ -25,7 +25,7 @@ div.full-size.pdf(ng-controller="PdfController")
dropdown-toggle
)
span.caret
ul.dropdown-menu.dropdown-menu-right
ul.dropdown-menu.dropdown-menu-left
li.dropdown-header #{translate("compile_mode")}
li
a(href, ng-click="draft = false")
@@ -36,6 +36,19 @@ div.full-size.pdf(ng-controller="PdfController")
i.fa.fa-fw(ng-class="{'fa-check': draft}")
| &nbsp;#{translate("fast")}&nbsp;
span.subdued [draft]
li.dropdown-header #{translate("compile_time_checks")}
li
a(href, ng-click="stop_on_validation_error = true")
i.fa.fa-fw(ng-class="{'fa-check': stop_on_validation_error}")
| &nbsp;#{translate("stop_on_validation_error")}
li
a(href, ng-click="stop_on_validation_error = false")
i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}")
| &nbsp;#{translate("ignore_validation_errors")}
li
a(href, ng-click="recompile({check:true})")
i.fa.fa-fw()
| &nbsp;#{translate("run_syntax_check_now")}
a(
href
ng-click="stop()"
@@ -100,6 +113,23 @@ div.full-size.pdf(ng-controller="PdfController")
strong #{translate("compile_error")}.
span #{translate("generic_failed_compile_message")}.
.alert.alert-danger(ng-show="pdf.failedCheck")
strong #{translate("failed_compile_check")}.
p
p.text-center(ng-show="!check")
a.text-info(
href,
ng-disabled="pdf.compiling",
ng-click="recompile({try:true})"
) #{translate("failed_compile_check_try")}
| &#32;#{translate("failed_compile_option_or")}&#32;
a.text-info(
href,
ng-disabled="pdf.compiling",
ng-click="recompile({force:true})"
) #{translate("failed_compile_check_ignore")}
| .
div(ng-repeat="entry in pdf.logEntries.all", ng-controller="PdfLogEntryController")
.alert(
ng-class="{\
@@ -115,7 +145,8 @@ div.full-size.pdf(ng-controller="PdfController")
| &nbsp;
span(ng-show="entry.file") {{ entry.file }}
span(ng-show="entry.line") , line {{ entry.line }}
p.entry-message(ng-show="entry.message") {{ entry.message }}
p.entry-message(ng-show="entry.message")
| {{ entry.type }} {{ entry.message }}
.card.card-hint(
ng-if="entry.humanReadableHint"
stop-propagation="click"
@@ -126,7 +157,7 @@ div.full-size.pdf(ng-controller="PdfController")
ng-show="entry.humanReadableHint",
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
.card-hint-actions.clearfix
.card-hint-ext-link(ng-if="wikiEnabled")
.card-hint-ext-link(ng-if="wikiEnabled && entry.extraInfoURL")
a(
ng-href="{{ entry.extraInfoURL }}",
ng-click="trackLogHintsLearnMore()"
@@ -198,7 +229,7 @@ div.full-size.pdf(ng-controller="PdfController")
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
p
div
.files-dropdown-container
a.btn.btn-default.btn-sm(
href,
@@ -313,7 +344,7 @@ div.full-size.pdf(ng-controller="PdfController")
strong #{translate("ask_proj_owner_to_upgrade_for_faster_compiles")}
p #{translate("free_accounts_have_timeout_upgrade_to_increase")}
p Plus:
p
div
ul.list-unstyled
li
i.fa.fa-check &nbsp;
@@ -27,12 +27,12 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
) #{translate("make_private")}
.row.project-member
.col-xs-8 {{ project.owner.email }}
.text-right(
.text-left(
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
) #{translate("owner")}
.row.project-member(ng-repeat="member in project.members")
.col-xs-8 {{ member.email }}
.col-xs-3.text-right
.col-xs-3.text-left
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
@@ -43,6 +43,23 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
ng-click="removeMember(member)"
)
i.fa.fa-times
.row.project-invite(ng-repeat="invite in project.invites")
.col-xs-8 {{ invite.email }}&nbsp;
div.small
| #{translate("invite_not_accepted")}.&nbsp;
a(href="#", ng-click="resendInvite(invite, $event)") #{translate("resend")}
.col-xs-3.text-left
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1
a(
href
tooltip="#{translate('revoke_invite')}"
tooltip-placement="bottom"
ng-click="revokeInvite(invite)"
)
i.fa.fa-times
.row.invite-controls
form(ng-show="canAddCollaborators")
.small #{translate("share_with_your_collabs")}
@@ -78,6 +95,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
button.btn.btn-info(
type="submit"
ng-mousedown="addMembers()"
ng-keyup="$event.keyCode == 13 ? addMembers() : null"
) #{translate("share")}
div(ng-hide="canAddCollaborators")
p.text-center #{translate("need_to_upgrade_for_more_collabs")}. Also:
@@ -123,7 +141,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
.modal-footer-left
i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")}
button.btn.btn-primary(
button.btn.btn-default(
ng-click="done()"
) #{translate("close")}
@@ -0,0 +1,18 @@
extends ../../layout
block content
.content.content-alt
.container
.row
.col-md-8.col-md-offset-2
.card.project-invite-invalid
.page-header.text-centered
h1 #{translate("invite_not_valid")}
.row.text-center
.col-md-12
p
| #{translate("invite_not_valid_description")}.
.row.text-center.actions
.col-md-12
a.btn.btn-info(href="/project") #{translate("back_to_your_projects")}
@@ -0,0 +1,31 @@
extends ../../layout
block content
.content.content-alt
.container
.row
.col-md-8.col-md-offset-2
.card.project-invite-accept
.page-header.text-centered
h1 #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})}
em
span.project-name #{project.name}
.row.text-center
.col-md-12
p
| #{translate("accepting_invite_as")}&nbsp;
em #{user.email}
.row
.col-md-12
form.form(
name="acceptForm",
method="POST",
action="/project/#{invite.projectId}/invite/token/#{invite.token}/accept"
)
input(name='_csrf', type='hidden', value=csrfToken)
input(name='token', type='hidden', value="#{invite.token}")
.form-group.text-center
button.btn.btn-lg.btn-primary(type="submit")
| #{translate("join_project")}
.form-group.text-center
+3 -3
View File
@@ -23,7 +23,7 @@ block content
.container
.row(ng-cloak)
span(ng-show="first_sign_up == 'default' || projects.length > 0")
span(ng-if="projects.length > 0")
aside.col-md-2.col-xs-3
include ./list/side-bar
@@ -31,8 +31,8 @@ block content
include ./list/notifications
include ./list/project-list
span(ng-if="first_sign_up == 'minimial' && projects.length == 0")
span(ng-if="projects.length === 0")
.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8
include ./list/project-list-minimal
include ./list/empty-project-list
include ./list/modals
@@ -0,0 +1,46 @@
.row.row-spaced
.col-xs-12
.card.card-thin.project-list-card
div.welcome.text-centered(ng-cloak)
h2 #{translate("welcome_to_sl")}
p #{translate("new_to_latex_look_at")}
a(href="/templates") #{translate("templates").toLowerCase()}
| #{translate("or")}
a(href="/learn") #{translate("latex_help_guide")}
.row
.col-md-offset-4.col-md-4
.dropdown.minimal-create-proj-dropdown(dropdown)
a.btn.btn-success.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| Create First Project
ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu")
li
a(
href,
ng-click="openCreateProjectModal()"
) #{translate("blank_project")}
li
a(
href,
ng-click="openCreateProjectModal('example')"
) #{translate("example_project")}
li
a(
href,
ng-click="openUploadProjectModal()"
) #{translate("upload_project")}
!= moduleIncludes("newProjectMenu", locals)
if (templates)
li.divider
li.dropdown-header #{translate("templates")}
each item in templates
li
a.menu-indent(href=item.url) #{translate(item.name)}
@@ -4,12 +4,31 @@ span(ng-controller="NotificationsController").userNotifications
ng-cloak
)
li.notification_entry(
ng-repeat="unreadNotification in notifications",
ng-repeat="notification in notifications",
)
.row(ng-hide="unreadNotification.hide")
.row(ng-hide="notification.hide")
.col-xs-12
.alert.alert-info
span(ng-bind-html="unreadNotification.html")
button(ng-click="dismiss(unreadNotification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.alert.alert-info(ng-if="notification.templateKey == 'notification_project_invite'", ng-controller="ProjectInviteNotificationController")
div.notification_inner
.notification_body(ng-show="!notification.accepted")
| !{translate("notification_project_invite_message")}
a.pull-right.btn.btn-sm.btn-info(href, ng-click="accept()", ng-disabled="notification.inflight")
span(ng-show="!notification.inflight") #{translate("join_project")}
span(ng-show="notification.inflight")
i.fa.fa-fw.fa-spinner.fa-spin
| &nbsp;
| #{translate("joining")}...
.notification_body(ng-show="notification.accepted")
| !{translate("notification_project_invite_accepted_message")}
a.pull-right.btn.btn-sm.btn-info(href="/project/{{ notification.messageOpts.projectId }}") #{translate("open_project")}
span().notification_close
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
.alert.alert-info(ng-if="notification.templateKey != 'notification_project_invite'")
div.notification_inner
span(ng-bind-html="notification.html").notification_body
span().notification_close
button(ng-click="dismiss(notification)").close.pull-right
span(aria-hidden="true") &times;
span.sr-only #{translate("close")}
@@ -1,202 +0,0 @@
.row
.col-xs-12(ng-cloak)
.project-tools(ng-cloak)
.btn-toolbar(ng-show="filter != 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href='#',
tooltip="#{translate('download')}",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="downloadSelectedProjects()"
)
i.fa.fa-cloud-download
a.btn.btn-default(
href='#',
tooltip="#{translate('delete')}",
tooltip-placement="bottom",
tooltip-append-to-body="true",
ng-click="openArchiveProjectsModal()"
)
i.fa.fa-trash-o
.btn-group.dropdown(ng-hide="selectedProjects.length < 1", dropdown)
a.btn.btn-default.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle,
tooltip="#{translate('add_to_folders')}",
tooltip-append-to-body="true",
tooltip-placement="bottom"
)
i.fa.fa-folder-open-o
|
span.caret
ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu(
role="menu"
ng-controller="TagListController"
)
li.dropdown-header #{translate("add_to_folder")}
li(
ng-repeat="tag in tags | filter:nonEmpty | orderBy:'name'",
ng-controller="TagDropdownItemController"
)
a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click")
i.fa(
ng-class="{\
'fa-check-square-o': areSelectedProjectsInTag == true,\
'fa-square-o': areSelectedProjectsInTag == false,\
'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\
}"
)
| {{tag.name}}
li.divider
li
a(href="#", ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")}
.btn-group(ng-hide="selectedProjects.length != 1", dropdown).dropdown
a.btn.btn-default.dropdown-toggle(
href='#',
data-toggle="dropdown",
dropdown-toggle
) #{translate("more")}
span.caret
ul.dropdown-menu.dropdown-menu-right(role="menu")
li(ng-show="getFirstSelectedProject().accessLevel == 'owner'")
a(
href='#',
ng-click="openRenameProjectModal()"
) #{translate("rename")}
li
a(
href='#',
ng-click="openCloneProjectModal()"
) #{translate("make_copy")}
.btn-toolbar(ng-show="filter == 'archived'")
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-default(
href='#',
data-original-title="Restore",
data-toggle="tooltip",
data-placement="bottom",
ng-click="restoreSelectedProjects()"
) #{translate("restore")}
.btn-group(ng-hide="selectedProjects.length < 1")
a.btn.btn-danger(
href='#',
data-original-title="Delete Forever",
data-toggle="tooltip",
data-placement="bottom",
ng-click="openDeleteProjectsModal()"
) #{translate("delete_forever")}
.row.row-spaced
.col-xs-12
.card.card-thin.project-list-card
ul.list-unstyled.project-list.structured-list(
select-all-list,
ng-if="projects.length > 0",
max-height="projectListHeight - 25",
ng-cloak
)
li.container-fluid
.row
.col-xs-6
input.select-all(
select-all,
type="checkbox"
)
span.header.clickable(ng-click="changePredicate('name')") #{translate("title")}
i.tablesort.fa(ng-class="getSortIconClass('name')")
.col-xs-2
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
.col-xs-4
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
li.project_entry.container-fluid(
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"
)
.row
.col-xs-6
input.select-item(
select-individual,
type="checkbox",
ng-model="project.selected"
)
span
a.projectName(href="/project/{{project.id}}") {{project.name}}
span(
ng-controller="TagListController"
)
a.label.label-default.tag-label(
href,
ng-repeat='tag in project.tags',
ng-click="selectTag(tag)"
) {{tag.name}}
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4
span.last-modified {{project.lastUpdated | formatDate}}
li(
ng-if="visibleProjects.length == 0",
ng-cloak
)
.row
.col-xs-12.text-centered
small #{translate("no_projects")}
div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak)
h2 #{translate("welcome_to_sl")}
p #{translate("new_to_latex_look_at")}
a(href="/templates") #{translate("templates").toLowerCase()}
| #{translate("or")}
a(href="/learn") #{translate("latex_help_guide")}
.row
.col-md-offset-4.col-md-4
.dropdown(dropdown)
a.btn.btn-success.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| Create First Project
style.
.dropdown{text-align:center;}
.button, .dropdown-menu{margin:2px auto}
.dropdown-menu{width:200px; left:50%; margin-left:-100px;}
ul.dropdown-menu(role="menu", style="text-align:center;")
li
a(
href,
ng-click="openCreateProjectModal()"
sixpack-convert="first_sign_up",
) #{translate("blank_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openCreateProjectModal('example')"
) #{translate("example_project")}
li
a(
href,
sixpack-convert="first_sign_up",
ng-click="openUploadProjectModal()"
) #{translate("upload_project")}
!= moduleIncludes("newProjectMenu", locals)
if (templates)
li.divider
li.dropdown-header #{translate("templates")}
each item in templates
li
a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)}
@@ -8,7 +8,7 @@
input.form-control.col-md-7.col-xs-12(
placeholder="#{translate('search_projects')}…",
autofocus='autofocus',
ng-model="searchText",
ng-model="searchText.value",
focus-on='search:clear',
ng-keyup="searchProjects()"
)
@@ -16,7 +16,7 @@
i.fa.fa-times.form-control-feedback(
ng-click="clearSearchText()",
style="cursor: pointer;",
ng-show="searchText.length > 0"
ng-show="searchText.value.length > 0"
)
//- i.fa.fa-remove
@@ -155,6 +155,6 @@
| #{translate("or_unlock_features_bonus")}
a(href="/user/bonus") #{translate("sharing_sl")} .
script.
window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)}
@@ -40,6 +40,11 @@ block content
.container(ng-controller="UserSubscriptionController")
.row
.col-md-8.col-md-offset-2
if saved_billing_details
.alert.alert-success
i.fa.fa-check
| &nbsp;
| #{translate("your_billing_details_were_saved")}
.card(ng-if="view == 'overview'")
.page-header
h1 #{translate("your_subscription")}
@@ -1,10 +1,9 @@
extends ../layout
block content
- locals.supressDefaultJs = true
script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
script(src=buildJsPath('libs/recurly.min.js'))
block scripts
script(src=buildJsPath('libs/recurly.min.js', {fingerprint:false}))
block content
.content.content-alt
.container
.row
@@ -19,7 +18,7 @@ block content
Recurly.config(!{recurlyConfig})
Recurly.buildBillingInfoUpdateForm({
target : "#billingDetailsForm",
successURL : "#{successURL}?_csrf=#{csrfToken}",
successURL : "#{successURL}?_csrf=#{csrfToken}&origin=editBillingDetails",
signature : "!{signature}",
accountCode : "#{user.id}"
});
@@ -3,7 +3,7 @@ extends ../../layout
block scripts
script(type='text/javascript').
window.group_subscription_id = '#{group_subscription_id}'
window.has_personal_subscription = '#{has_personal_subscription}'
window.has_personal_subscription = #{has_personal_subscription}
block content
.content.content-alt
+193 -205
View File
@@ -1,6 +1,6 @@
extends ../layout
block scripts
block scripts
script(src="https://js.recurly.com/v3/recurly.js")
script(type='text/javascript').
@@ -11,10 +11,6 @@ block scripts
window.couponCode = "#{couponCode}"
block content
- locals.supressDefaultJs = true
script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
.content.content-alt
.container(ng-controller="NewSubscriptionController" ng-cloak)
.row.card-group
@@ -51,226 +47,218 @@ block content
div(ng-if="normalPrice")
span.small Normally {{price.currency.symbol}}{{normalPrice}}
.row
.col-md-12
form(ng-show="planName")
div()
.col-md-12()
form(
ng-if="planName"
name="simpleCCForm"
novalidate
)
div.payment-method-toggle
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('credit_card');"
ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-mastercard.fa-2x
span &nbsp;
i.fa.fa-cc-visa.fa-2x
span &nbsp;
i.fa.fa-cc-amex.fa-2x
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('paypal');"
ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-paypal.fa-2x
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
.row
.col-md-12
.form-group
.row
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
i.fa.fa-cc-mastercard.fa-3x
span &nbsp;
i.fa.fa-cc-visa.fa-3x
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
i.fa.fa-cc-paypal.fa-3x
div(ng-if="paymentMethod.value === 'credit_card'")
.row
.col-xs-6
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
label(for="first-name") #{translate('first_name')}
input#first-name.form-control(
type="text"
maxlength='255'
data-recurly="first_name"
name="firstName"
ng-model="data.first_name"
required
)
span.input-feedback-message {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
.col-xs-6
.form-group(for="last-name",ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
label(for="last-name") #{translate('last_name')}
input#last-name.form-control(
type="text"
maxlength='255'
data-recurly="last_name"
name="lastName"
ng-model="data.last_name"
required
)
span.input-feedback-message {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
type="text"
ng-model="data.number"
name="ccNumber"
ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
ng-blur="validateCardNumber();"
required
cc-format-card-number
)
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
.row
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
label #{translate("expiry")}
input.form-control(
type="text"
ng-model="data.mmYY"
name="expiry"
placeholder="MM / YY"
ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
ng-blur="updateExpiry(); validateExpiry()"
required
cc-format-expiry
)
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
label #{translate("security_code")}
input.form-control(
type="text"
ng-model="data.cvv"
ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
ng-blur="validateCvv()"
name="cvv"
required
cc-format-sec-code
)
.form-control-feedback
a.form-helper(
href
tabindex="-1"
tooltip-template="'cvv-tooltip-tpl.html'"
tooltip-trigger="mouseenter"
tooltip-append-to-body="true"
) ?
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
span(ng-hide="paymentMethod == 'paypal'")
.row
.col-md-12
.form-group
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
.row
.col-md-6
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
.col-md-3
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
.row
.col-md-12
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
.row
.col-md-3
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
option(value="", disabled, selected) Month
option(value="01") 01
option(value="02") 02
option(value="03") 03
option(value="04") 04
option(value="05") 05
option(value="06") 06
option(value="07") 07
option(value="08") 08
option(value="09") 09
option(value="10") 10
option(value="11") 11
option(value="12") 12
.col-md-4
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
option(value="", disabled, selected) Year
option(value="2016") 2016
option(value="2017") 2017
option(value="2018") 2018
option(value="2019") 2019
option(value="2020") 2020
option(value="2021") 2021
option(value="2022") 2022
option(value="2023") 2023
option(value="2024") 2024
option(value="2025") 2025
option(value="2026") 2026
.row
.col-md-6
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
.col-md-6
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
hr
.row
.col-md-12
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
label #{translate("billing_address")}
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
.row
.col-md-7
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
.row
.col-md-7
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
div
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
name="country"
ng-change="updateCountry()"
required
)
mixin countries_options()
.row
.col-md-8
if (showCouponField)
.form-group
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
.row
.col-md-8
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
if (showVatField)
.form-group
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
.row
.col-xs-7
.form-group
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
.col-xs-3.pricingBreakdown
div(ng-if="price.next.subtotal != price.next.total") Subtotal
div(ng-if="price.next.tax!='0.00'") Tax
div
strong Total
.col-xs-2
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
label(for="vat-no") #{translate('vat_number')}
input#vat-no.form-control(
type="text"
ng-blur="applyVatNumber()"
ng-model="data.vat_number"
)
if (showCouponField)
.form-group
label(for="coupon-code") #{translate('coupon_code')}
input#coupon-code.form-control(
type="text"
ng-blur="applyCoupon()"
ng-model="data.coupon"
)
p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")}
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
strong {{price.currency.symbol}}{{price.next.total}}
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
hr.thin
span(sixpack-switch="payment-left-menu-bottom")
div.payment-submit
button.btn.btn-success.btn-block(
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
)
span(ng-show="processing")
i.fa.fa-spinner.fa-spin
| &nbsp;
| {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
.col-md-3.col-md-pull-4(sixpack-default)
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
h3 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
.col-md-3.col-md-pull-4
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
h3 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
h3
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
h3
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
h3 #{translate("full_doc_history")}
p #{translate("see_what_has_been")}
span.added #{translate("added")}
| #{translate("and")}
span.removed #{translate("removed")}.
| #{translate("restore_to_any_older_version")}.
h3 #{translate("sync_to_dropbox")}
p
| #{translate("acces_work_from_anywhere")}.
| #{translate("work_offline_and_sync_with_dropbox")}.
hr
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
hr
span &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
.col-md-3.col-md-pull-4(sixpack-when="bolder")
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
.page-header
h2 #{translate("features")}
h3
i.fa.fa-check &nbsp;
| #{translate("unlimited_projects")}
h3 #{translate("full_doc_history")}
p #{translate("see_what_has_been")}
span.added #{translate("added")}
| #{translate("and")}
span.removed #{translate("removed")}.
| #{translate("restore_to_any_older_version")}.
h3
i.fa.fa-check &nbsp;
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
h3 #{translate("sync_to_dropbox")}
p
| #{translate("acces_work_from_anywhere")}.
| #{translate("work_offline_and_sync_with_dropbox")}.
h3
i.fa.fa-check &nbsp;
| #{translate("full_doc_history")}
hr
h3
i.fa.fa-check &nbsp;
| #{translate("sync_to_dropbox")}
h3
i.fa.fa-check &nbsp;
| #{translate("sync_to_github")}
h3
i.fa.fa-check &nbsp;
| #{translate("Compile Larger Projects")}
hr
h2.text-center 30 Day Guarantee
hr
span &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
hr
span &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
script(type="text/javascript").
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
script(
type="text/ng-template"
id="cvv-tooltip-tpl.html"
)
p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
mixin countries_options()
option(value='', disabled, selected) #{translate("country")}
option(value='-') --------------
@@ -72,7 +72,10 @@ block content
li &nbsp;
li
br
a.btn.btn-info(href="/register") #{translate("sign_up_now")}
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
.card-header
@@ -136,7 +139,10 @@ block content
li &nbsp;
li
br
a.btn.btn-info(href="/register") #{translate("sign_up_now")}
a.btn.btn-info(
href="/register"
style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
) #{translate("sign_up_now")}
.col-md-4
.card.card-highlighted
+7 -3
View File
@@ -6,9 +6,13 @@ block content
.row
.registration_message
if sharedProjectData.user_first_name !== undefined
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:sharedProjectData.project_name})}
div #{translate("join_sl_to_view_project")}.
a(href="/login") #{translate("login_here")}
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})}
em #{sharedProjectData.project_name}
div
| #{translate("join_sl_to_view_project")}.
div
| #{translate("if_you_are_registered")},
a(href="/login?redir=#{getReqQueryParam('redir')}") #{translate("login_here")}
else if newTemplateData.templateName !== undefined
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
+49
View File
@@ -0,0 +1,49 @@
extends ../layout
block scripts
script(type='text/javascript').
window.otherSessions = !{JSON.stringify(sessions)}
block content
.content.content-alt
.container
.row
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
.card.clear-user-sessions(ng-controller="ClearSessionsController", ng-cloak)
.page-header
h1 #{translate("your_sessions")}
div
p.small
| #{translate("clear_sessions_description")}
div
div(ng-if="state.otherSessions.length == 0")
p.text-center
| #{translate("no_other_sessions")}
div(ng-if="state.success == true")
p.text-success.text-center
| #{translate('clear_sessions_success')}
div(ng-if="state.otherSessions.length != 0")
table.table.table-striped
thead
tr
th #{translate("ip_address")}
th #{translate("session_created_at")}
tr(ng-repeat="session in state.otherSessions")
td {{session.ip_address}}
td {{session.session_created | formatDate}}
p.actions
.text-center
button.btn.btn-lg.btn-primary(
ng-click="clearSessions()"
) #{translate('clear_sessions')}
div(ng-if="state.error == true")
p.text-danger.error
| #{translate('generic_something_went_wrong')}
+23 -12
View File
@@ -79,7 +79,7 @@ block content
required,
complex-password
)
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.currentPassword.$dirty", ng-bind-html="complexPasswordErrorMessage")
span.small.text-primary(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage")
.form-group
label(for='newPassword2') #{translate("confirm_new_password")}
input.form-control(
@@ -88,9 +88,11 @@ block content
placeholder='*********',
ng-model="newPassword2",
equals="passwordField"
)
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
| #{translate("doesnt_match")}
)
span.small.text-primary(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty")
| #{translate("doesnt_match")}
span.small.text-primary(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty")
| #{translate("Invalid Password")}
.actions
button.btn.btn-primary(
type='submit',
@@ -99,16 +101,25 @@ block content
| !{moduleIncludes("userSettings", locals)}
hr
h3
| #{translate("sharelatex_beta_program")}
if (user.betaProgram)
hr
h3
| #{translate("sharelatex_beta_program")}
p.small
| #{translate("beta_program_already_participating")}
div
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
div
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
hr
h3
| #{translate("sessions")}
div
a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
hr
@@ -140,7 +151,7 @@ block content
.modal-header
h3 #{translate("delete_account")}
.modal-body
p !{translate("delete_account_warning_message")}
p !{translate("delete_account_warning_message_2")}
form(novalidate, name="deleteAccountForm")
input.form-control(
type="text",
+8 -6
View File
@@ -106,8 +106,8 @@ module.exports = settings =
url: ""
# references:
# url: "http://localhost:3040"
# notifications:
# url: "http://localhost:3042"
notifications:
url: "http://localhost:3042"
templates:
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
@@ -116,7 +116,7 @@ module.exports = settings =
# cdn:
# web:
# host:"http://cdn.sharelatex.dev:3000"
# host:"http://nowhere.sharelatex.dev"
# darkHost:"http://cdn.sharelatex.dev:3000"
# Where your instance of ShareLaTeX can be found publically. Used in emails
@@ -155,7 +155,7 @@ module.exports = settings =
collaborators: -1
dropbox: true
versioning: true
compileTimeout: 60
compileTimeout: 180
compileGroup: "standard"
references: true
templates: true
@@ -166,6 +166,8 @@ module.exports = settings =
price: 0
features: defaultFeatures
}]
enableSubscriptions:false
# i18n
# ------
@@ -193,8 +195,8 @@ module.exports = settings =
# passwordStrengthOptions:
# pattern: "aA$3"
# length:
# min: 8
# max: 50
# min: 1
# max: 10
# Email support
# -------------
+1111 -833
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -46,6 +46,10 @@
"nodemailer-sendgrid-transport": "^0.2.0",
"nodemailer-ses-transport": "^1.3.0",
"optimist": "0.6.1",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"pg": "^6.0.3",
"pg-hstore": "^2.3.2",
"redback": "0.4.0",
"redis": "0.10.1",
"redis-sharelatex": "0.0.9",
@@ -53,6 +57,7 @@
"requests": "^0.1.7",
"rimraf": "2.2.6",
"sanitizer": "0.1.1",
"sequelize": "^3.2.0",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
"sixpack-client": "^1.0.0",
"temp": "^0.8.3",
@@ -112,6 +112,8 @@ define [
[asyncFormCtrl, ngModelCtrl] = ctrl
ngModelCtrl.$parsers.unshift (modelValue) ->
isValid = passField.validatePass()
email = asyncFormCtrl.getEmail() || window.usersEmail
if !isValid
@@ -121,5 +123,8 @@ define [
if modelValue.indexOf(email) != -1 or modelValue.indexOf(startOfEmail) != -1
isValid = false
scope.complexPasswordErrorMessage = "Password can not contain email address"
if opts.length.max? and modelValue.length == opts.length.max
isValid = false
scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached"
ngModelCtrl.$setValidity('complexPassword', isValid)
return modelValue
@@ -0,0 +1,539 @@
define [
"base"
], (App) ->
App.factory 'ccUtils', () ->
defaultFormat = /(\d{1,4})/g;
defaultInputFormat = /(?:^|\s)(\d{4})$/
cards = [
# Credit cards
{
type: 'visa'
patterns: [4]
format: defaultFormat
length: [13, 16]
cvcLength: [3]
luhn: true
}
{
type: 'mastercard'
patterns: [
51, 52, 53, 54, 55,
22, 23, 24, 25, 26, 27
]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'amex'
patterns: [34, 37]
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
length: [15]
cvcLength: [3..4]
luhn: true
}
{
type: 'dinersclub'
patterns: [30, 36, 38, 39]
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
length: [14]
cvcLength: [3]
luhn: true
}
{
type: 'discover'
patterns: [60, 64, 65, 622]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'unionpay'
patterns: [62, 88]
format: defaultFormat
length: [16..19]
cvcLength: [3]
luhn: false
}
{
type: 'jcb'
patterns: [35]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
]
cardFromNumber = (num) ->
num = (num + '').replace(/\D/g, "")
for card in cards
for pattern in card.patterns
p = pattern + ""
return card if num.substr(0, p.length) == p
cardFromType = (type) ->
return card for card in cards when card.type is type
cardType = (num) ->
return null unless num
cardFromNumber(num)?.type or null
formatCardNumber = (num) ->
num = num.replace(/\D/g, '')
card = cardFromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num[0...upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
return unless groups?
groups.shift()
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
formatExpiry = (expiry) ->
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
return '' unless parts
mon = parts[1] || ''
sep = parts[2] || ''
year = parts[3] || ''
if year.length > 0
sep = ' / '
else if sep is ' /'
mon = mon.substring(0, 1)
sep = ''
else if mon.length == 2 or sep.length > 0
sep = ' / '
else if mon.length == 1 and mon not in ['0', '1']
mon = "0#{mon}"
sep = ' / '
return mon + sep + year
parseExpiry = (value = "") ->
[month, year] = value.split(/[\s\/]+/, 2)
# Allow for year shortcut
if year?.length is 2 and /^\d+$/.test(year)
prefix = (new Date).getFullYear()
prefix = prefix.toString()[0..1]
year = prefix + year
month = parseInt(month, 10)
year = parseInt(year, 10)
return unless !isNaN(month) and !isNaN(year)
month: month, year: year
return {
fromNumber: cardFromNumber
fromType: cardFromType
cardType: cardType
formatExpiry: formatExpiry
formatCardNumber: formatCardNumber
defaultFormat: defaultFormat
defaultInputFormat: defaultInputFormat
parseExpiry: parseExpiry
}
App.factory 'ccFormat', (ccUtils, $filter) ->
hasTextSelected = ($target) ->
# If some text is selected
return true if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt $target.prop('selectionEnd')
# If some text is selected in IE
if document?.selection?.createRange?
return true if document.selection.createRange().text
false
safeVal = (value, $target) ->
try
cursor = $target.prop('selectionStart')
catch error
cursor = null
last = $target.val()
$target.val(value)
if cursor != null && $target.is(":focus")
cursor = value.length if cursor is last.length
# This hack looks for scenarios where we are changing an input's value such
# that "X| " is replaced with " |X" (where "|" is the cursor). In those
# scenarios, we want " X|".
#
# For example:
# 1. Input field has value "4444| "
# 2. User types "1"
# 3. Input field has value "44441| "
# 4. Reformatter changes it to "4444 |1"
# 5. By incrementing the cursor, we make it "4444 1|"
#
# This is awful, and ideally doesn't go here, but given the current design
# of the system there does not appear to be a better solution.
#
# Note that we can't just detect when the cursor-1 is " ", because that
# would incorrectly increment the cursor when backspacing, e.g. pressing
# backspace in this scenario: "4444 1|234 5".
if last != value
prevPair = last[cursor-1..cursor]
currPair = value[cursor-1..cursor]
digit = value[cursor]
cursor = cursor + 1 if /\d/.test(digit) and
prevPair == "#{digit} " and currPair == " #{digit}"
$target.prop('selectionStart', cursor)
$target.prop('selectionEnd', cursor)
# Replace Full-Width Chars
replaceFullWidthChars = (str = '') ->
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
halfWidth = '0123456789'
value = ''
chars = str.split('')
# Avoid using reserved word `char`
for chr in chars
idx = fullWidth.indexOf(chr)
chr = halfWidth[idx] if idx > -1
value += chr
value
# Format Numeric
reFormatNumeric = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')
safeVal(value, $target)
# Format Card Number
reFormatCardNumber = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = ccUtils.formatCardNumber(value)
safeVal(value, $target)
formatCardNumber = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
value = $target.val()
card = ccUtils.fromNumber(value + digit)
length = (value.replace(/\D/g, '') + digit).length
upperLength = 16
upperLength = card.length[card.length.length - 1] if card
return if length >= upperLength
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
if card && card.type is 'amex'
# AMEX cards are formatted differently
re = /^(\d{4}|\d{4}\s\d{6})$/
else
re = /(?:^|\s)(\d{4})$/
# If '4242' + 4
if re.test(value)
e.preventDefault()
setTimeout -> $target.val(value + ' ' + digit)
# If '424' + 2
else if re.test(value + digit)
e.preventDefault()
setTimeout -> $target.val(value + digit + ' ')
formatBackCardNumber = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the digit + trailing space
if /\d\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s$/, ''))
# Remove digit if ends in space + digit
else if /\s\d?$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d$/, ''))
getFormattedCardNumber = (num) ->
num = num.replace(/\D/g, '')
card = ccUtils.fromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num[0...upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
return unless groups?
groups.shift()
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
parseCardNumber = (value) ->
if value? then value.replace(/\s/g, '') else value
# Format Expiry
reFormatExpiry = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = ccUtils.formatExpiry(value)
safeVal(value, $target)
formatExpiry = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val() + digit
if /^\d$/.test(val) and val not in ['0', '1']
e.preventDefault()
setTimeout -> $target.val("0#{val} / ")
else if /^\d\d$/.test(val)
e.preventDefault()
setTimeout ->
# Split for months where we have the second digit > 2 (past 12) and turn
# that into (m1)(m2) => 0(m1) / (m2)
m1 = parseInt(val[0], 10)
m2 = parseInt(val[1], 10)
if m2 > 2 and m1 != 0
$target.val("0#{m1} / #{m2}")
else
$target.val("#{val} / ")
formatForwardExpiry = (e) ->
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val()
if /^\d\d$/.test(val)
$target.val("#{val} / ")
formatForwardSlash = (e) ->
which = String.fromCharCode(e.which)
return unless which is '/' or which is ' '
$target = $(e.currentTarget)
val = $target.val()
if /^\d$/.test(val) and val isnt '0'
$target.val("0#{val} / ")
formatBackExpiry = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the trailing space + last digit
if /\d\s\/\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
parseExpiry = (value) ->
if value?
dateAsObj = ccUtils.parseExpiry(value)
return unless dateAsObj?
expiry = new Date dateAsObj.year, dateAsObj.month - 1
return $filter('date')(expiry, 'MM/yyyy')
# Format CVC
reFormatCVC = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')[0...4]
safeVal(value, $target)
# Restrictions
restrictNumeric = (e) ->
# Key event is for a browser shortcut
return true if e.metaKey or e.ctrlKey
# If keycode is a space
return false if e.which is 32
# If keycode is a special char (WebKit)
return true if e.which is 0
# If char is a special char (Firefox)
return true if e.which < 33
input = String.fromCharCode(e.which)
# Char is a number or a space
!!/[\d\s]/.test(input)
restrictCardNumber = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
# Restrict number of digits
value = ($target.val() + digit).replace(/\D/g, '')
card = ccUtils.fromNumber(value)
if card
value.length <= card.length[card.length.length - 1]
else
# All other cards are 16 digits long
value.length <= 16
restrictExpiry = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
value = $target.val() + digit
value = value.replace(/\D/g, '')
return false if value.length > 6
restrictCVC = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
val = $target.val() + digit
val.length <= 4
setCardType = (e) ->
$target = $(e.currentTarget)
val = $target.val()
cardType = ccUtils.cardType(val) or 'unknown'
unless $target.hasClass(cardType)
allTypes = (card.type for card in cards)
$target.removeClass('unknown')
$target.removeClass(allTypes.join(' '))
$target.addClass(cardType)
$target.toggleClass('identified', cardType isnt 'unknown')
$target.trigger('payment.cardType', cardType)
return {
hasTextSelected
replaceFullWidthChars
reFormatNumeric
reFormatCardNumber
formatCardNumber
formatBackCardNumber
getFormattedCardNumber
parseCardNumber
reFormatExpiry
formatExpiry
formatForwardExpiry
formatForwardSlash
formatBackExpiry
parseExpiry
reFormatCVC
restrictNumeric
restrictCardNumber
restrictExpiry
restrictCVC
setCardType
}
App.directive "ccFormatExpiry", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictExpiry
el.on "keypress", ccFormat.formatExpiry
el.on "keypress", ccFormat.formatForwardSlash
el.on "keypress", ccFormat.formatForwardExpiry
el.on "keydown", ccFormat.formatBackExpiry
el.on "change", ccFormat.reFormatExpiry
el.on "input", ccFormat.reFormatExpiry
el.on "paste", ccFormat.reFormatExpiry
ngModel.$parsers.push ccFormat.parseExpiry
ngModel.$formatters.push ccFormat.parseExpiry
App.directive "ccFormatCardNumber", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictCardNumber
el.on "keypress", ccFormat.formatCardNumber
el.on "keydown", ccFormat.formatBackCardNumber
el.on "paste", ccFormat.reFormatCardNumber
ngModel.$parsers.push ccFormat.parseCardNumber
ngModel.$formatters.push ccFormat.getFormattedCardNumber
App.directive "ccFormatSecCode", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictCVC
el.on "paste", ccFormat.reFormatCVC
el.on "change", ccFormat.reFormatCVC
el.on "input", ccFormat.reFormatCVC

Some files were not shown because too many files have changed in this diff Show More