Merge branch 'master' into i18n

Conflicts:
	package.json
This commit is contained in:
Henry Oswald
2014-04-07 09:53:03 +01:00
79 changed files with 931 additions and 807 deletions
+2
View File
@@ -63,6 +63,8 @@ public/minjs/
public/js/main.js
Gemfile.lock
app/views/external
*.swp
.DS_Store
+1 -4
View File
@@ -17,10 +17,7 @@ Unit test status
License and Credits
-------------------
### Code
All coffeescript files (files ended in *.coffee) are licensed under the
[AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html).
This project is licensed under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html)
### Stylesheets
+38 -14
View File
@@ -33,26 +33,50 @@ processingFuncs =
else
callback()
pipeStreamFrom: (options, callback)->
pipeStreamFrom: (options, _callback)->
callback = (args...) ->
_callback(args...)
_callback = () ->
if options.filePath == "/droppy/main.tex"
request options.streamOrigin, (err,res, body)->
logger.log options:options, body:body
origin = request(options.streamOrigin)
cancelled = false
gotResponse = false
origin.on 'response', (res) ->
return if cancelled
gotResponse = true
if 200 <= res.statusCode < 300
dest = request(options)
origin.pipe(dest)
dest.on "error", (err)->
logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest"
callback(err)
dest.on 'end', callback
else
error = new Error("received non-success status code: #{res.statusCode}")
logger.error err: error, options: options, "something went wrong connecting to origin"
callback(error)
origin.on 'error', (err)->
return if cancelled
gotResponse = true
logger.error err:err, options:options, "something went wrong in pipeStreamFrom origin"
if err?
callback(err)
else
callback()
dest = request(options)
origin.pipe(dest)
dest.on "error", (err)->
logger.error err:err, options:options, "something went wrong in pipeStreamFrom dest"
if err?
callback(err)
else
callback()
dest.on 'end', callback
callback(err)
setTimeout () ->
return if gotResponse
cancelled = true
error = new Error("timeout")
logger.error err: error, options: options, "timeout"
callback(error)
, 2000
workerRegistration = (groupKey, method, options, callback)->
+1 -1
View File
@@ -15,7 +15,7 @@ argv = require("optimist")
.argv
Server.app.use (error, req, res, next) ->
logger.error err: error
logger.error err: error, url:req.url, method:req.method, user:req?.sesson?.user, "error passed to top level next middlewear"
res.statusCode = error.status or 500
if res.statusCode == 500
res.end("Oops, something went wrong with your request, sorry. If this continues, please contact us at team@sharelatex.com")
@@ -44,7 +44,11 @@ module.exports = AuthenticationController =
res.send(auth_token)
getLoggedInUserId: (req, callback = (error, user_id) ->) ->
callback null, req.session.user._id.toString()
if req?.session?.user?._id?
callback null, req.session.user._id.toString()
else
e = new Error("user is not on req session")
callback e
getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) ->
if req.session?.user?._id?
@@ -19,6 +19,9 @@ module.exports = CompileController =
res.header('Content-Disposition', "filename=#{project.getSafeProjectName()}.pdf")
CompileController.proxyToClsi("/project/#{project_id}/output/output.pdf", req, res, next)
deleteAuxFiles: (req, res, next) ->
project_id = req.params.Project_id
CompileController.proxyToClsi("/project/#{project_id}", req, res, next)
compileAndDownloadPdf: (req, res, next)->
project_id = req.params.project_id
@@ -29,8 +32,6 @@ module.exports = CompileController =
url = "/project/#{project_id}/output/output.pdf"
CompileController.proxyToClsi url, req, res, next
getFileFromClsi: (req, res, next = (error) ->) ->
CompileController.proxyToClsi("/project/#{req.params.Project_id}/output/#{req.params.file}", req, res, next)
@@ -38,8 +39,8 @@ module.exports = CompileController =
logger.log url: url, "proxying to CLSI"
url = "#{Settings.apis.clsi.url}#{url}"
oneMinute = 60 * 1000
proxy = request.get(url: url, timeout: oneMinute)
proxy = request(url: url, method: req.method, timeout: oneMinute)
proxy.pipe(res)
proxy.on "error", (error) ->
logger.error err: error, url: url, "CLSI proxy error"
logger.warn err: error, url: url, "CLSI proxy error"
@@ -1,6 +1,6 @@
logger = require('logger-sharelatex')
Metrics = require('../../infrastructure/Metrics')
sanitize = require('validator').sanitize
sanitize = require('sanitizer')
ProjectEditorHandler = require('../Project/ProjectEditorHandler')
ProjectEntityHandler = require('../Project/ProjectEntityHandler')
ProjectOptionsHandler = require('../Project/ProjectOptionsHandler')
@@ -163,7 +163,7 @@ module.exports = EditorController =
addDoc: (project_id, folder_id, docName, docLines, sl_req_id, callback = (error, doc)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
docName = sanitize(docName).xss()
docName = sanitize.escape(docName)
logger.log sl_req_id:sl_req_id, "sending new doc to project #{project_id}"
Metrics.inc "editor.add-doc"
ProjectEntityHandler.addDoc project_id, folder_id, docName, docLines, sl_req_id, (err, doc, folder_id)=>
@@ -172,7 +172,7 @@ module.exports = EditorController =
addFile: (project_id, folder_id, fileName, path, sl_req_id, callback = (error, file)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
fileName = sanitize(fileName).xss()
fileName = sanitize.escape(fileName)
logger.log sl_req_id:sl_req_id, "sending new file to project #{project_id} with folderid: #{folder_id}"
Metrics.inc "editor.add-file"
ProjectEntityHandler.addFile project_id, folder_id, fileName, path, (err, fileRef, folder_id)=>
@@ -185,7 +185,7 @@ module.exports = EditorController =
addFolder: (project_id, folder_id, folderName, sl_req_id, callback = (error, folder)->)->
{callback, sl_req_id} = slReqIdHelper.getCallbackAndReqId(callback, sl_req_id)
folderName = sanitize(folderName).xss()
folderName = sanitize.escape(folderName)
logger.log "sending new folder to project #{project_id}"
Metrics.inc "editor.add-folder"
ProjectEntityHandler.addFolder project_id, folder_id, folderName, (err, folder, folder_id)=>
@@ -237,6 +237,44 @@ module.exports = EditorController =
EditorRealTimeController.emitToRoom(project_id, 'projectDescriptionUpdated', description)
callback()
deleteProject: (project_id, callback)->
Metrics.inc "editor.delete-project"
logger.log project_id:project_id, "recived message to delete project"
ProjectHandler.deleteProject project_id, callback
renameEntity: (project_id, entity_id, entityType, newName, callback)->
newName = sanitize.escape(newName)
Metrics.inc "editor.rename-entity"
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
ProjectHandler.renameEntity project_id, entity_id, entityType, newName, =>
if newName.length > 0
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
callback?()
moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
Metrics.inc "editor.move-entity"
ProjectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
callback?()
renameProject: (project_id, window_id, newName, callback)->
newName = sanitize.escape(newName)
ProjectHandler.renameProject project_id, window_id, newName, =>
newName = sanitize.escape(newName)
EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', window_id, newName
callback?()
setPublicAccessLevel : (project_id, newAccessLevel, callback)->
ProjectHandler.setPublicAccessLevel project_id, newAccessLevel, =>
EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel
callback?()
setRootDoc: (project_id, newRootDocID, callback)->
ProjectEntityHandler.setRootDoc project_id, newRootDocID, () =>
EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID
callback?()
p:
notifyProjectUsersOfNewFolder: (project_id, folder_id, folder, callback = (error)->)->
logger.log project_id:project_id, folder:folder, parentFolder_id:folder_id, "sending newly created folder out to users"
@@ -1,4 +1,5 @@
ProjectGetter = require("./ProjectGetter")
UserGetter = require("../User/UserGetter")
Project = require('../../models/Project').Project
logger = require("logger-sharelatex")
@@ -9,12 +10,15 @@ module.exports =
if err?
logger.err err:err, project_id:project_id, "error getting project"
return callback(err)
details =
name : project.name
description: project.description
compiler: project.compiler
logger.log project_id:project_id, details:details, "getting project details"
callback(err, details)
UserGetter.getUser project.owner_ref, (err, user) ->
return callback(err) if err?
details =
name : project.name
description: project.description
compiler: project.compiler
features: user.features
logger.log project_id:project_id, details:details, "getting project details"
callback(err, details)
setProjectDescription: (project_id, description, callback)->
conditions = _id:project_id
@@ -28,6 +28,9 @@ module.exports =
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserLocator.findById user_id, (err, user)->
if err? or !user?
users.push _id:user_id
return cb()
userViewModel = buildUserViewModel(user)
users.push(userViewModel)
cb()
@@ -38,6 +38,8 @@ module.exports =
buildViewModel : ->
plans = Settings.plans
console.log plans
console.log(typeof(plans))
allPlans = {}
plans.forEach (plan)->
allPlans[plan.planCode] = plan
@@ -23,7 +23,7 @@ self = module.exports =
callback()
_getUserIdsWithDropbox: (callback)->
User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id", (err, users)->
User.find {"dropbox.access_token.oauth_token_secret":{"$exists":true}, "features.dropbox":true}, "_id", (err, users)->
ids = users.map (user)->
return user._id+""
callback err, ids
@@ -5,7 +5,7 @@ logger = require('logger-sharelatex')
Settings = require('settings-sharelatex')
slReqIdHelper = require('soa-req-id')
FileTypeManager = require('../Uploads/FileTypeManager')
GuidManager = require '../../managers/GuidManager'
uuid = require('node-uuid')
fs = require('fs')
module.exports =
@@ -75,7 +75,7 @@ module.exports =
writeStreamToDisk: (project_id, file_id, stream, callback = (err, fsPath)->)->
if !file_id?
file_id = GuidManager.newGuid()
file_id = uuid.v4()
dumpPath = "#{Settings.path.dumpFolder}/#{project_id}_#{file_id}"
writeStream = fs.createWriteStream(dumpPath)
@@ -1,5 +1,5 @@
User = require("../../models/User").User
NewsletterManager = require "../../managers/NewsletterManager"
NewsletterManager = require "../Newsletter/NewsletterManager"
ProjectDeleter = require("../Project/ProjectDeleter")
logger = require("logger-sharelatex")
SubscriptionHandler = require("../Subscription/SubscriptionHandler")
@@ -1,4 +1,4 @@
sanitize = require('validator').sanitize
sanitize = require('sanitizer')
module.exports =
validateEmail : (email) ->
@@ -13,7 +13,7 @@ module.exports =
return hasZeroLength
validateRegisterRequest : (req, callback)->
email = sanitize(req.body.email).xss().trim().toLowerCase()
email = sanitize.escape(req.body.email).trim().toLowerCase()
password = req.body.password
username = email.match(/^[^@]*/)
if username?
@@ -3,8 +3,11 @@ _ = require('underscore')
User = require('./UserController')
Quotes = require('../models/Quote').Quote
Path = require "path"
fs = require "fs"
homepageExists = fs.existsSync Path.resolve(__dirname + "/../../views/external/home.jade")
module.exports =
module.exports = HomeController =
index : (req,res)->
if req.session.user
if req.query.scribtex_path?
@@ -12,42 +15,23 @@ module.exports =
else
res.redirect '/project'
else
res.render 'homepage/home',
title: 'ShareLaTeX.com'
if homepageExists
res.render 'external/home',
title: 'ShareLaTeX.com'
else
res.redirect "/login"
comments : (req, res)->
res.render 'homepage/comments.jade',
title: 'User Comments'
resources : (req, res)->
res.render 'resources.jade',
title: 'LaTeX Resources'
tos : (req, res) ->
res.render 'about/tos',
title: "Terms of Service"
privacy : (req, res) ->
res.render 'about/privacy',
title: "Privacy Policy"
about : (req, res) ->
res.render 'about/about',
title: "About us"
externalPage: (page, title) ->
return (req, res, next = (error) ->) ->
path = Path.resolve(__dirname + "/../../views/external/#{page}.jade")
fs.exists path, (exists) -> # No error in this callback - old method in Node.js!
if exists
res.render "external/#{page}.jade",
title: title
else
HomeController.notFound(req, res, next)
notFound: (req, res)->
res.statusCode = 404
res.render 'general/404',
title: "Page Not Found"
security : (req, res) ->
res.render 'about/security',
title: "Security"
attribution: (req, res) ->
res.render 'about/attribution',
title: "Attribution"
planned_maintenance: (req, res) ->
res.render 'about/planned_maintenance',
title: "Planned Maintenance"
title: "Page Not Found"
@@ -1,13 +1,12 @@
User = require('../models/User').User
Project = require('../models/Project').Project
sanitize = require('validator').sanitize
sanitize = require('sanitizer')
path = require "path"
logger = require('logger-sharelatex')
_ = require('underscore')
fs = require('fs')
ProjectHandler = require '../handlers/ProjectHandler'
SecurityManager = require '../managers/SecurityManager'
GuidManager = require '../managers/GuidManager'
Settings = require('settings-sharelatex')
projectCreationHandler = require '../Features/Project/ProjectCreationHandler'
projectDuplicator = require('../Features/Project/ProjectDuplicator')
@@ -19,7 +18,7 @@ SubscriptionFormatters = require("../Features/Subscription/SubscriptionFormatter
FileStoreHandler = require("../Features/FileStore/FileStoreHandler")
module.exports = class ProjectController
constructor: (@collaberationManager)->
constructor: ()->
ProjectHandler = new ProjectHandler()
list: (req, res, next)->
@@ -72,8 +71,8 @@ module.exports = class ProjectController
apiNewProject: (req, res)->
user = req.session.user
projectName = sanitize(req.body.projectName).xss()
template = sanitize(req.body.template).xss()
projectName = sanitize.escape(req.body.projectName)
template = sanitize.escape(req.body.template)
logger.log user: user, type: template, name: projectName, "creating project"
if template == 'example'
projectCreationHandler.createExampleProject user._id, projectName, (err, project)->
@@ -123,7 +122,7 @@ module.exports = class ProjectController
trackChanges: false
else
anonymous = false
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
SubscriptionLocator.getUsersSubscription user?._id, (err, subscription)->
SecurityManager.userCanAccessProject user, project, (canAccess, privlageLevel)->
allowedFreeTrial = true
if subscription? and subscription.freeTrial? and subscription.freeTrial.expiresAt?
@@ -154,7 +153,7 @@ module.exports = class ProjectController
spellCheckLanguage: user.ace.spellCheckLanguage
pdfViewer : user.ace.pdfViewer
docPositions: {}
trackChanges: user.featureSwitches.trackChanges
oldHistory: !!user.featureSwitches?.oldHistory
})
sharelatexObject : JSON.stringify({
siteUrl: Settings.siteUrl,
@@ -1,11 +1,11 @@
User = require('../models/User').User
sanitize = require('validator').sanitize
sanitize = require('sanitizer')
fs = require('fs')
_ = require('underscore')
logger = require('logger-sharelatex')
Security = require('../managers/SecurityManager')
Settings = require('settings-sharelatex')
newsLetterManager = require('../managers/NewsletterManager')
newsLetterManager = require('../Features/Newsletter/NewsletterManager')
dropboxHandler = require('../Features/Dropbox/DropboxHandler')
userRegistrationHandler = require('../Features/User/UserRegistrationHandler')
metrics = require('../infrastructure/Metrics')
@@ -95,8 +95,8 @@ module.exports =
title: 'Password Reset',
doRequestPasswordReset : (req, res, next = (error) ->)->
email = sanitize(req.body.email).xss()
email = sanitize(email).trim()
email = sanitize.escape(req.body.email)
email = sanitize.escape(email).trim()
email = email.toLowerCase()
logger.log email: email, "password reset requested"
User.findOne {'email':email}, (err, user)->
@@ -156,11 +156,11 @@ module.exports =
metrics.inc "user.settings-update"
User.findById req.session.user._id, (err, user)->
if(user)
user.first_name = sanitize(req.body.first_name).xss().trim()
user.last_name = sanitize(req.body.last_name).xss().trim()
user.ace.mode = sanitize(req.body.mode).xss().trim()
user.ace.theme = sanitize(req.body.theme).xss().trim()
user.ace.fontSize = sanitize(req.body.fontSize).xss().trim()
user.first_name = sanitize.escape(req.body.first_name).trim()
user.last_name = sanitize.escape(req.body.last_name).trim()
user.ace.mode = sanitize.escape(req.body.mode).trim()
user.ace.theme = sanitize.escape(req.body.theme).trim()
user.ace.fontSize = sanitize.escape(req.body.fontSize).trim()
user.ace.autoComplete = req.body.autoComplete == "true"
user.ace.spellCheckLanguage = req.body.spellCheckLanguage
user.ace.pdfViewer = req.body.pdfViewer
@@ -171,7 +171,7 @@ module.exports =
metrics.inc "user.password-change"
oldPass = req.body.currentPassword
AuthenticationManager.authenticate _id: req.session.user._id, oldPass, (err, user)->
return callback(err) if err?
return next(err) if err?
if(user)
logger.log user: req.session.user, "changing password"
newPassword1 = req.body.newPassword1
@@ -197,23 +197,23 @@ module.exports =
type:'error'
text:'Your old password is wrong'
redirectUserToDropboxAuth: (req, res)->
redirectUserToDropboxAuth: (req, res, next)->
user_id = req.session.user._id
dropboxHandler.getDropboxRegisterUrl user_id, (err, url)->
return callback(err) if err?
return next(err) if err?
logger.log url:url, "redirecting user for dropbox auth"
res.redirect url
completeDropboxRegistration: (req, res)->
completeDropboxRegistration: (req, res, next)->
user_id = req.session.user._id
dropboxHandler.completeRegistration user_id, (err, success)->
return callback(err) if err?
return next(err) if err?
res.redirect('/user/settings#dropboxSettings')
unlinkDropbox: (req, res)->
unlinkDropbox: (req, res, next)->
user_id = req.session.user._id
dropboxHandler.unlinkAccount user_id, (err, success)->
return callback(err) if err?
return next(err) if err?
res.redirect('/user/settings#dropboxSettings')
deleteUser: (req, res)->
@@ -1,62 +0,0 @@
#this file is being slowly refactored out
logger = require('logger-sharelatex')
sanitize = require('validator').sanitize
projectHandler = require('../handlers/ProjectHandler')
projectHandler = new projectHandler()
SecurityManager = require('./SecurityManager')
_ = require('underscore')
projectEditorHandler = require('../Features/Project/ProjectEditorHandler')
projectEntityHandler = require('../Features/Project/ProjectEntityHandler')
versioningApiHandler = require('../Features/Versioning/VersioningApiHandler')
metrics = require('../infrastructure/Metrics')
EditorRealTimeController = require('../Features/Editor/EditorRealTimeController')
module.exports = class CollaberationManager
constructor: (@io)->
deleteProject: (project_id, callback)->
metrics.inc "editor.delete-project"
logger.log project_id:project_id, "recived message to delete project"
projectHandler.deleteProject project_id, callback
renameEntity: (project_id, entity_id, entityType, newName, callback)->
newName = sanitize(newName).xss()
metrics.inc "editor.rename-entity"
logger.log entity_id:entity_id, entity_id:entity_id, entity_id:entity_id, "reciving new name for entity for project"
projectHandler.renameEntity project_id, entity_id, entityType, newName, =>
if newName.length > 0
EditorRealTimeController.emitToRoom project_id, 'reciveEntityRename', entity_id, newName
callback?()
moveEntity: (project_id, entity_id, folder_id, entityType, callback)->
metrics.inc "editor.move-entity"
projectEntityHandler.moveEntity project_id, entity_id, folder_id, entityType, =>
EditorRealTimeController.emitToRoom project_id, 'reciveEntityMove', entity_id, folder_id
callback?()
renameProject: (project_id, window_id, newName, callback)->
newName = sanitize(newName).xss()
projectHandler.renameProject project_id, window_id, newName, =>
newName = sanitize(newName).xss()
EditorRealTimeController.emitToRoom project_id, 'projectNameUpdated', window_id, newName
callback?()
setPublicAccessLevel : (project_id, newAccessLevel, callback)->
projectHandler.setPublicAccessLevel project_id, newAccessLevel, =>
EditorRealTimeController.emitToRoom project_id, 'publicAccessLevelUpdated', newAccessLevel
callback?()
distributMessage: (project_id, client, message)->
message = sanitize(message).xss()
metrics.inc "editor.instant-message"
client.get "first_name", (err, first_name)=>
EditorRealTimeController.emitToRoom project_id, 'reciveNewMessage', first_name, message
setRootDoc: (project_id, newRootDocID, callback)->
projectEntityHandler.setRootDoc project_id, newRootDocID, () =>
EditorRealTimeController.emitToRoom project_id, 'rootDocUpdated', newRootDocID
callback?()
takeVersionSnapShot : (project_id, message, callback)->
versioningApiHandler.takeVersionSnapshot project_id, message, callback
@@ -1,6 +0,0 @@
module.exports =
newGuid : ()->
S4 = ()->
return (((1+Math.random())*0x10000)|0).toString(16).substring(1)
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4())
@@ -166,7 +166,9 @@ getRequestUserAndProject = (req, res, options, callback)->
callback err, user, project
getProjectIdFromRef = (ref)->
if ref._id?
if !ref?
return null
else if ref._id?
return ref._id+''
else
return ref+''
@@ -3,7 +3,7 @@ Settings = require 'settings-sharelatex'
_ = require('underscore')
FolderSchema = require('./Folder.js').FolderSchema
logger = require('logger-sharelatex')
sanitize = require('validator').sanitize
sanitize = require('sanitizer')
concreteObjectId = require('mongoose').Types.ObjectId
Errors = require "../errors"
@@ -112,7 +112,7 @@ applyToAllFilesRecursivly = ProjectSchema.statics.applyToAllFilesRecursivly = (f
ProjectSchema.methods.getSafeProjectName = ->
safeProjectName = this.name.replace(new RegExp("\\W", "g"), '_')
return sanitize(safeProjectName).xss()
return sanitize.escape(safeProjectName)
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: Settings.mongo.poolSize || 10)
+1 -1
View File
@@ -32,7 +32,7 @@ UserSchema = new Schema
}
featureSwitches : {
dropbox: {type:Boolean, default:true},
trackChanges: {type:Boolean, default:false}
oldHistory: {type:Boolean}
}
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ]
+45 -54
View File
@@ -5,8 +5,7 @@ ProjectController = require("./controllers/ProjectController")
ProjectApiController = require("./Features/Project/ProjectApiController")
InfoController = require('./controllers/InfoController')
SpellingController = require('./Features/Spelling/SpellingController')
CollaberationManager = require('./managers/CollaberationManager')
SecutiryManager = require('./managers/SecurityManager')
SecurityManager = require('./managers/SecurityManager')
AuthorizationManager = require('./Features/Security/AuthorizationManager')
versioningController = require("./Features/Versioning/VersioningApiController")
EditorController = require("./Features/Editor/EditorController")
@@ -46,9 +45,7 @@ module.exports = class Router
constructor: (app, io, socketSessions)->
app.use(app.router)
collaberationManager = new CollaberationManager(io)
Project = new ProjectController(collaberationManager)
Project = new ProjectController()
projectHandler = new ProjectHandler()
app.get '/', HomeController.index
@@ -56,16 +53,16 @@ module.exports = class Router
app.get '/login', UserController.loginForm
app.post '/login', AuthenticationController.login
app.get '/logout', UserController.logout
app.get '/restricted', SecutiryManager.restricted
app.get '/restricted', SecurityManager.restricted
app.get '/resources', HomeController.externalPage("resources", "LaTeX Resources")
app.get '/tos', HomeController.externalPage("tos", "Terms of Service")
app.get '/about', HomeController.externalPage("about", "About Us")
app.get '/attribution', HomeController.externalPage("attribution", "Attribution")
app.get '/security', HomeController.externalPage("security", "Security")
app.get '/privacy_policy', HomeController.externalPage("privacy", "Privacy Policy")
app.get '/planned_maintenance', HomeController.externalPage("planned_mainteance", "Planned Maintenance")
app.get '/resources', HomeController.resources
app.get '/comments', HomeController.comments
app.get '/tos', HomeController.tos
app.get '/about', HomeController.about
app.get '/attribution', HomeController.attribution
app.get '/security', HomeController.security
app.get '/privacy_policy', HomeController.privacy
app.get '/planned_maintenance', HomeController.planned_maintenance
app.get '/themes', InfoController.themes
app.get '/advisor', InfoController.advisor
app.get '/dropbox', InfoController.dropbox
@@ -99,13 +96,10 @@ module.exports = class Router
app.post '/project/new', AuthenticationController.requireLogin(), Project.apiNewProject
app.get '/project/new/template', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.createProjectFromZipTemplate
app.get '/Project/:Project_id', SecutiryManager.requestCanAccessProject, Project.loadEditor
app.get '/Project/:Project_id/file/:File_id', SecutiryManager.requestCanAccessProject, FileStoreController.getFile
app.get '/Project/:Project_id', SecurityManager.requestCanAccessProject, Project.loadEditor
app.get '/Project/:Project_id/file/:File_id', SecurityManager.requestCanAccessProject, FileStoreController.getFile
# This is left for legacy reasons and can be removed once all editors have had a chance to refresh:
app.get '/Project/:Project_id/download/pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf
app.get '/Project/:Project_id/output/output.pdf', SecutiryManager.requestCanAccessProject, CompileController.downloadPdf
app.get '/Project/:Project_id/output/output.pdf', SecurityManager.requestCanAccessProject, CompileController.downloadPdf
app.get /^\/project\/([^\/]*)\/output\/(.*)$/,
((req, res, next) ->
params =
@@ -113,25 +107,26 @@ module.exports = class Router
"file": req.params[1]
req.params = params
next()
), SecutiryManager.requestCanAccessProject, CompileController.getFileFromClsi
), SecurityManager.requestCanAccessProject, CompileController.getFileFromClsi
app.del "/project/:Project_id/output", SecurityManager.requestCanAccessProject, CompileController.deleteAuxFiles
app.del '/Project/:Project_id', SecutiryManager.requestIsOwner, Project.deleteProject
app.post '/Project/:Project_id/clone', SecutiryManager.requestCanAccessProject, Project.cloneProject
app.del '/Project/:Project_id', SecurityManager.requestIsOwner, Project.deleteProject
app.post '/Project/:Project_id/clone', SecurityManager.requestCanAccessProject, Project.cloneProject
app.post '/Project/:Project_id/snapshot', SecutiryManager.requestCanModifyProject, versioningController.takeSnapshot
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
app.get '/Project/:Project_id/version', SecutiryManager.requestCanAccessProject, versioningController.listVersions
app.get '/Project/:Project_id/version/:Version_id', SecutiryManager.requestCanAccessProject, versioningController.getVersion
app.post '/Project/:Project_id/snapshot', SecurityManager.requestCanModifyProject, versioningController.takeSnapshot
app.get '/Project/:Project_id/version', SecurityManager.requestCanAccessProject, versioningController.listVersions
app.get '/Project/:Project_id/version/:Version_id', SecurityManager.requestCanAccessProject, versioningController.getVersion
app.get '/Project/:Project_id/version', SecurityManager.requestCanAccessProject, versioningController.listVersions
app.get '/Project/:Project_id/version/:Version_id', SecurityManager.requestCanAccessProject, versioningController.getVersion
app.get "/project/:Project_id/updates", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get "/project/:Project_id/doc/:doc_id/diff", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecutiryManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get "/project/:Project_id/updates", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.get "/project/:Project_id/doc/:doc_id/diff", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", SecurityManager.requestCanAccessProject, TrackChangesController.proxyToTrackChangesApi
app.post '/project/:project_id/leave', AuthenticationController.requireLogin(), CollaboratorsController.removeSelfFromProject
app.get '/project/:Project_id/collaborators', SecutiryManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
app.get '/project/:Project_id/collaborators', SecurityManager.requestCanAccessProject(allow_auth_token: true), CollaboratorsController.getCollaborators
app.get '/Project/:Project_id/download/zip', SecutiryManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
app.get '/Project/:Project_id/download/zip', SecurityManager.requestCanAccessProject, ProjectDownloadsController.downloadProject
app.get '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
@@ -164,21 +159,21 @@ module.exports = class Router
req.params = params
next()
),
SecutiryManager.requestCanAccessProject, versioningController.getVersionFile
SecurityManager.requestCanAccessProject, versioningController.getVersionFile
app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
#Admin Stuff
app.get '/admin', SecutiryManager.requestIsAdmin, AdminController.index
app.post '/admin/closeEditor', SecutiryManager.requestIsAdmin, AdminController.closeEditor
app.post '/admin/dissconectAllUsers', SecutiryManager.requestIsAdmin, AdminController.dissconectAllUsers
app.post '/admin/writeAllDocsToMongo', SecutiryManager.requestIsAdmin, AdminController.writeAllToMongo
app.post '/admin/addquote', SecutiryManager.requestIsAdmin, AdminController.addQuote
app.post '/admin/syncUserToSubscription', SecutiryManager.requestIsAdmin, AdminController.syncUserToSubscription
app.post '/admin/flushProjectToTpds', SecutiryManager.requestIsAdmin, AdminController.flushProjectToTpds
app.post '/admin/pollUsersWithDropbox', SecutiryManager.requestIsAdmin, AdminController.pollUsersWithDropbox
app.post '/admin/updateProjectCompiler', SecutiryManager.requestIsAdmin, AdminController.updateProjectCompiler
app.get '/admin', SecurityManager.requestIsAdmin, AdminController.index
app.post '/admin/closeEditor', SecurityManager.requestIsAdmin, AdminController.closeEditor
app.post '/admin/dissconectAllUsers', SecurityManager.requestIsAdmin, AdminController.dissconectAllUsers
app.post '/admin/writeAllDocsToMongo', SecurityManager.requestIsAdmin, AdminController.writeAllToMongo
app.post '/admin/addquote', SecurityManager.requestIsAdmin, AdminController.addQuote
app.post '/admin/syncUserToSubscription', SecurityManager.requestIsAdmin, AdminController.syncUserToSubscription
app.post '/admin/flushProjectToTpds', SecurityManager.requestIsAdmin, AdminController.flushProjectToTpds
app.post '/admin/pollUsersWithDropbox', SecurityManager.requestIsAdmin, AdminController.pollUsersWithDropbox
app.post '/admin/updateProjectCompiler', SecurityManager.requestIsAdmin, AdminController.updateProjectCompiler
app.get '/perfTest', (req,res)->
res.send("hello")
@@ -190,7 +185,7 @@ module.exports = class Router
app.get '/health_check', HealthCheckController.check
app.get "/status/compiler/:Project_id", SecutiryManager.requestCanAccessProject, (req, res) ->
app.get "/status/compiler/:Project_id", SecurityManager.requestCanAccessProject, (req, res) ->
sendRes = _.once (statusCode, message)->
res.writeHead statusCode
res.end message
@@ -293,15 +288,15 @@ module.exports = class Router
client.on 'renameEntity', (entity_id, entityType, newName, callback)->
AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
collaberationManager.renameEntity(project_id, entity_id, entityType, newName, callback)
EditorController.renameEntity(project_id, entity_id, entityType, newName, callback)
client.on 'moveEntity', (entity_id, folder_id, entityType, callback)->
AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
collaberationManager.moveEntity(project_id, entity_id, folder_id, entityType, callback)
EditorController.moveEntity(project_id, entity_id, folder_id, entityType, callback)
client.on 'setProjectName', (window_id, newName, callback)->
AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
collaberationManager.renameProject(project_id, window_id, newName, callback)
EditorController.renameProject(project_id, window_id, newName, callback)
client.on 'getProject',(callback)->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
@@ -309,15 +304,15 @@ module.exports = class Router
client.on 'setRootDoc', (newRootDocID, callback)->
AuthorizationManager.ensureClientCanEditProject client, (error, project_id) =>
collaberationManager.setRootDoc(project_id, newRootDocID, callback)
EditorController.setRootDoc(project_id, newRootDocID, callback)
client.on 'deleteProject', (callback)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
collaberationManager.deleteProject(project_id, callback)
EditorController.deleteProject(project_id, callback)
client.on 'setPublicAccessLevel', (newAccessLevel, callback)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
collaberationManager.setPublicAccessLevel(project_id, newAccessLevel, callback)
EditorController.setPublicAccessLevel(project_id, newAccessLevel, callback)
client.on 'pdfProject', (opts, callback)->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
@@ -328,10 +323,6 @@ module.exports = class Router
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
CompileManager.getLogLines project_id, callback
client.on 'distributMessage', (message)->
AuthorizationManager.ensureClientCanViewProject client, (error, project_id) =>
collaberationManager.distributMessage project_id, client, message
client.on 'changeUsersPrivlageLevel', (user_id, newPrivalageLevel)->
AuthorizationManager.ensureClientCanAdminProject client, (error, project_id) =>
projectHandler.changeUsersPrivlageLevel project_id, user_id, newPrivalageLevel
-41
View File
@@ -1,41 +0,0 @@
extends ../layout
block content
.container
.row
.span8.offset2.span-box
.page-header
h1 About us
h3 Meet the team behind your favourite online LaTeX editor
p.team-profile
img(src='/img/about/henry_oswald.jpg')
strong Henry Oswald
| built an experimental LaTeX editor in 2011 which later became ShareLaTeX. He is a trained software engineer who lives in London.
| Henry has been responsible for building up a reliable platform for ShareLaTeX that allows instant real-time collaboration.
| Henry is a strong advocate of Test Driven Development and makes sure we keep the ShareLaTeX code clean and easy to maintain.
p
a(href='https://twitter.com/henryoswald') Follow me on Twitter
p.team-profile
img(src='/img/about/james_allen.jpg')
strong James Allen
| started working with Henry early in 2012 and finished his PhD in theoretical physics early in 2013. James began working on
| one of the first online LaTeX editors, ScribTeX, in 2008 and he has played a large role in developing the technologies and
| concepts that made ScribTeX and now ShareLaTeX possible. James is also slightly too obsessed with typography and learning the internals of how LaTeX works.
p(style="clear: both")
h3 Motivation
p Our first priority with ShareLaTeX is to build a tool which makes life easier for all the LaTeX users out there.
| The "thank you"s and success stories are a strong motivator for us, and we hope to always be able to offer a fully-functional
| LaTeX editor which anyone can use for free.
p We also believe that charging money for tools like ShareLaTeX is important since it helps to guarantee the future of the site and
| the safety of your work, as well as allowing us to focus on it full time to develop new features. We prefer to provide a valuable
| service at a fair price rather than having to try run the site from adverts, or worse. As our customers, we are answerable to you and only you.
h3 Technologies
p
| We use a lot of exciting technologies to run ShareLaTeX. We have mutliple APIs behind the scenes that we've written in Node.js, but the one exception is our
a(href='https://github.com/scribtex/clsi') open sourced compiler API.
| This is written in Ruby on Rails. We generally write in
a(href='http://coffeescript.org/') coffee-script
| as we find it helps to speed up development and make our code more readable. Your data is stored in MongoDB, Redis and Amazon S3. We practice Test Driven Development (TDD) to help produce robust and clean code which is easy to refactor.
include ../general/small-footer
@@ -1,31 +0,0 @@
extends ../layout
block content
.container
.row
.span6.offset3.span-box
.page-header
h1 Attribution
p
| We've only been able to create ShareLaTeX thanks to the many amazing free and
| open source technologies that exist. Here are some that we have been using and would
| like to say thank you to:
ul
li
a(href="http://nodejs.org/") Node.js
| and the massive set of modules that are available.
li
a(href="https://github.com/jviereck/pdfListView") pdfListView.
| A wrapper for PDF.js that powers our built in PDF viewer.
li
a(href="http://www.iconshock.com/") Icons from Iconshock.
li
a(href="http://twitter.github.com/bootstrap/") Bootstrap
| for letting us survive without being designers.
include ../general/small-footer
@@ -1,16 +0,0 @@
extends ../layout
block content
.container
.row
.span6.offset3.span-box
.page-header
h1 Planned Maintenance
p There is currently no planned maintenance
include ../general/small-footer
-26
View File
@@ -1,26 +0,0 @@
extends ../layout
block content
.container
.row
.span8.offset2.span-box
.page-header
h1 Privacy Policy
h3 Information gathering
p When you register for ShareLaTeX we collect information such as your name and email address. ShareLaTeX uses this information to be able to provide our service, for identification and authorization of users, and to be able to contact you.
p The information we collect is not shared with other organisations except as detailed below for the provisioning and improvement of our service. The data we collect will never be sold to third parties for commercial purposes.
h3 Cookies
p ShareLaTeX uses a cookie, which is a small amount of data stored by your web browser on your computer. The cookie stores your current session and allows you to stay logged in. Cookies are required to use the ShareLaTeX service.
h3 Data storage
p ShareLaTeX uses third parties to host our services and store your data. You retain all rights to the data you upload to ShareLaTeX.
h3 Credit cards and billing details
p We use a third party vendor,
a(href="recurly.com") Recurly
| , to store and process credit card transactions. Your email address, credit card details and billing address are passed on to Recurly and are not stored with ShareLaTeX.
h3 Third party tracking
p We use Google Analytics, Mixpanel and HeapAnalytics to track users' interactions with ShareLaTeX. This data is used for the improvement of our service.
include ../general/small-footer
@@ -1,94 +0,0 @@
extends ../layout
block content
.container
.row
.span6.offset3.span-box
.page-header
h1 Security
p
| Keeping your data safe is one of our top priorities.
| We work hard to make sure that ShareLaTeX is as secure as we can make it,
| and your input and feedback on our security is always appreciated.
h3 Responsible disclosure
p
| Please send reports of any urgent or sensitive security issues to
a(href="mailto:team@sharelatex.com") team@sharelatex.com.
| Use our
a(href="/sharelatex-security.pub") public key
| to encrypt your message and please provide us with a secure way to
| contact you.
p
| Note that the URLs at <strong>/learn</strong>, <strong>/help</strong>
| and <strong>ctan.sharelatex.com</strong> are not under our direct control,
| and vulnerabilities should be reported to either MediaWiki or TenderApp
| respectively.
p
| We are very grateful for all the responsibly reported security vulnerabilities,
| however listing on the hall of fame is reserved for people who report
| vulnerabilities that were previously unknown and we regard as serious.
h3 Acknowledgements
p
| We'd like to thank the following people who have responsibly disclosed
| vulnerabilities to us and helped improved the security of ShareLaTeX:
ul
li
a(href="https://twitter.com/Abdulahhusam", rel="nofollow") Abdullah Hussam Gazi
li
a(href="http://adamziaja.com", rel="nofollow") Adam Ziaja
li
a(href="http://alihassanpenetrationtester.blogspot.com/", rel="nofollow") Ali Hasan Ghauri
li
a(href="https://twitter.com/EhArvindSingh") Arvind Singh Shekhawat
li
a(href="https://www.facebook.com/Dakshxss", rel="nofollow") Daksh Patel
li
a(href="https://twitter.com/dibsyhex", rel="nofollow") Dibyendu Sikdar
li
a(href="https://www.facebook.com/jaymark.pestano", rel="nofollow") Jaymark Pestaño
li
a(href="https://twitter.com/korapsyon", rel="nofollow") Jerold Camacho
li
a(href="https://twitter.com/kamilsevi", rel="nofollow") Kamil Sevi
li
a(href="https://twitter.com/kingkaustubhp", rel="nofollow") Kaustubh Padwad
li 'KoF2002' &amp; 'Sr33h4r!(XSS no0B)'
li
a(href="http://twitter.com/umenmactech", rel="nofollow") Manish Bhattacharya
li
a(href="http://twitter.com/Manjesh24", rel="nofollow") Manjesh S
li
a(href="https://www.facebook.com/Shahmeer.1994", rel="nofollow") Muhammad Shahmeer
li
a(href="http://nbsriharsha.blogspot.in", rel="nofollow") N B Sri Harsha
li
a(href="http://www.linkedin.com/in/osandamalith", rel="nofollow") Osanda Malith Jayathissa
li
a(href="https://twitter.com/prasadk14", rel="nofollow") Prasad Kancharla
li
a(href="https://www.facebook.com/c0m4dr3404", rel="nofollow") Praveen Nair (Kerala Cyber Squad - India)
li
a(href="https://twitter.com/iAmPr3m", rel="nofollow") Prem Kumar
li
a(href="https://www.facebook.com/HardNocksHittnHard", rel="nofollow") Sherin Panikar (Kerala Cyber Squad - India)
li
a(href="https://twitter.com/Simon90_Italy", rel="nofollow") Simone Memoli
li
a(href="https://twitter.com/tareksiddiki", rel="nofollow") Tarek Siddiki
li
a(href="https://twitter.com/venugopalt", rel="nofollow") Venugopal Thotakura
li
a(href="http://softproweb.blogspot.com/", rel="nofollow") Waqeeh Ul Hasan
li
a(href="https://twitter.com/zerodayguys", rel="nofollow") Zeroday Guys (Rakesh Singh &amp; V.Harish Kumar)
li
a(href="https://twitter.com/_prashantnegi", rel="nofollow") Prashant Negi
span and
a(href="https://twitter.com/AjaySinghNegi", rel="nofollow") Singh Negi
li
a(href="https://twitter.com/mohitnitrr") Monendra Sahu
include ../general/small-footer
-34
View File
@@ -1,34 +0,0 @@
extends ../layout
block content
.container
.row
.span8.offset2.span-box
.page-header
h1 Terms of Service
p
| Thank you for taking the time to read our terms of service.
| We've tried to keep it simple, but you must agree to these terms in order to use ShareLaTeX.
h3 You must provide a valid email address
p
| We expect you to register with a valid email address.
| This will be our only way to communicate with you.
| Changes to these terms of service and any other notifications about updates
| to the service will be sent to the email address you provide.
| We will not be held responsible for any information not being received.
h3 You own your content, not us
p You retain all ownership, copyright and intellectual property rights to any content uploaded to ShareLaTeX. Your content will only be shared with other users of your choosing and we will never share your content with third parties without your consent. The staff of ShareLaTeX have access to your content, but we make an effort to only access it when absolutely necessary.
h3 You're not allowed to abuse the service
p ShareLaTeX is provided assuming users will act in good faith. However, we retain the right to remove any account or content that we feel is abusing the service. In the rare event of this happening, it will most likely be for one of the following reasons: using the service for illegal reasons; uploading illegal, unauthorised or objectionable content; consuming an excessive amount of computing resources.
h3 We're not infallible
p ShareLaTeX is provided on an 'as is' basis, without future promise of availability. ShareLaTeX is not liable for any damages caused by the loss or inability to access your data.
p [Founder's note: We try hard to provide a service which is reliable, secure and regularly backed up. While the above paragraph sounds grim, we need it there to cover ourselves incase something does go wrong. We certainly aren't planning to need to fall back on it. This assurance aside, it's always good practice to back up your own data.]
h3 Cancellation and refunds policy
p ShareLaTeX's services are paid for monthly or yearly in advance and are non-refundable. There will be no refunds or credits for partial months/years of service or downgrades. Subscription downgrades and cancellations will come into effect when your next payment is due. Upgrades to your plan will be billed immediately at the new rate for the remainder of the billing period. You may cancel your account at any time from your account subscription settings.
h3 Paypal Reference Transactions
p PayPal Reference Transactions are used to allow ShareLaTeX to make subsequent transactions on a monthly or annual basis until you cancel your account.
h3 Pricing changes
p We will notify you of any changes to our pricing plans that affect you at least 30 days before your next payment is due. We will notify you via the email address you provided when subscribing for a paid plan.
h3 You must be human
p To protect ourselves from accidental or malicious abuse, automated scripts and programs are not permitted to register an account or access the service.
include ../general/small-footer
@@ -1,46 +0,0 @@
include header
mixin quote(quote, author)
blockquote
p #{quote}
small #{author}
extends ../layout
block content
.container.box
.page-header
h2 What a few of the thousands of people using sharelatex are saying
.row
.span4
<blockquote class="twitter-tweet w-align-left"><p>Yes, finally! @<a href="https://twitter.com/sharelatex">sharelatex</a> <a href="https://t.co/5qcwmrdW" title="https://www.sharelatex.com/">sharelatex.com</a> via @<a href="https://twitter.com/henryoswald">henryoswald</a></p>&mdash; Nav (@keyboardkitteh) <a href="https://twitter.com/keyboardkitteh/status/165623092637474816" data-datetime="2012-02-04T02:29:57+00:00">February 4, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet w-align-left"><p>Finally someone programmed it: The Real Time LaTeX Collaborative Editor in Your Browser <a href="https://t.co/Ke32JVdG" title="https://www.sharelatex.com">sharelatex.com</a></p>&mdash; Bar Fooz (@barfooz) <a href="https://twitter.com/barfooz/status/165889791538376705" data-datetime="2012-02-04T20:09:43+00:00">February 4, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p>Wow, really awesome real time LaTeX editor <a href="https://t.co/VgXQezzr" title="https://www.sharelatex.com/">sharelatex.com</a> /via @<a href="https://twitter.com/cybunk">cybunk</a></p>&mdash; Nicolas (@philogb) <a href="https://twitter.com/philogb/status/168030573229649921" data-datetime="2012-02-10T17:56:25+00:00">February 10, 2012</a></blockquote>
.row
.span4
<blockquote class="twitter-tweet"><p>Its <a href="https://twitter.com/search/%2523GoogleDocs">#GoogleDocs</a> but with added <a href="https://twitter.com/search/%2523LaTeX">#LaTeX</a>, <a href="https://t.co/ZkMh44V2" title="https://www.sharelatex.com/">sharelatex.com</a>. For physicists and astronomers, collaborative editing just became possible.</p>&mdash; Alasdair Allan (@aallan) <a href="https://twitter.com/aallan/status/166472955314966528" data-datetime="2012-02-06T10:47:00+00:00">February 6, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p>ShareLaTeX, a real time LaTeX collaborative editor. I *have* to try this. <a href="https://t.co/UVi5f3XY" title="https://www.sharelatex.com/">sharelatex.com</a> HT: @<a href="https://twitter.com/yokofakun">yokofakun</a></p>&mdash; Luis Apiolaza (@zentree) <a href="https://twitter.com/zentree/status/169896014604546048" data-datetime="2012-02-15T21:29:01+00:00">February 15, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p>Finally! <a href="https://twitter.com/search/%2523ShareLaTeX">#ShareLaTeX</a> - Real Time LaTeX Collaborative Editor <a href="http://t.co/DYEaIOLi" title="http://ShareLaTeX.com">ShareLaTeX.com</a> (via @<a href="https://twitter.com/infoveille">infoveille</a> @<a href="https://twitter.com/gallypette">gallypette</a> @<a href="https://twitter.com/UrfistNice">UrfistNice</a>) @<a href="https://twitter.com/AdrienSaladin">AdrienSaladin</a></p>&mdash; Pierre Poulain (@pierrepo) <a href="https://twitter.com/pierrepo/status/167642895946498048" data-datetime="2012-02-09T16:15:55+00:00">February 9, 2012</a></blockquote>
.row
.span4
<blockquote class="twitter-tweet"><p>LaTeX geeks, here's a collaborative online LaTeX editor <a href="https://t.co/5xjcRbSy" title="https://www.sharelatex.com/">sharelatex.com</a></p>&mdash; Stephen Kinsella (@stephenkinsella) <a href="https://twitter.com/stephenkinsella/status/166838934968598529" data-datetime="2012-02-07T11:01:16+00:00">February 7, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p><a href="https://t.co/uRiailxg" title="https://www.sharelatex.com/">sharelatex.com</a>amazing!</p>&mdash; Guillermo Rauch (@rauchg) <a href="https://twitter.com/rauchg/status/168031797047865344" data-datetime="2012-02-10T18:01:16+00:00">February 10, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p>ShareLatex <a href="https://t.co/70tEkjEb" title="https://www.sharelatex.com/">sharelatex.com</a> FInally, the real time collaboration in browser! <a href="https://twitter.com/search/%2523latex">#latex</a></p>&mdash; Ivana Kojovic (@muvica1987) <a href="https://twitter.com/muvica1987/status/166200765638119424" data-datetime="2012-02-05T16:45:25+00:00">February 5, 2012</a></blockquote>
.row
.span4
<blockquote class="twitter-tweet"><p>I just fell in love on my first run with @<a href="https://twitter.com/sharelatex">sharelatex</a> <a href="https://t.co/fygg14Nj" title="https://www.sharelatex.com/">sharelatex.com</a> via @<a href="https://twitter.com/henryoswald">henryoswald</a> . CC @<a href="https://twitter.com/timcameronryan">timcameronryan</a></p>&mdash; Cory Dolphin (@wcdolphin) <a href="https://twitter.com/wcdolphin/status/200411830370312192" data-datetime="2012-05-10T02:27:58+00:00">May 10, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p>Tired of sending TeX files back and forth with your coauthors?Try sharelatex to collaborate on tex files in real time <a href="http://t.co/PjoOAayE" title="http://bit.ly/xcksY7">bit.ly/xcksY7</a></p>&mdash; Max Lin (@m4xl1n) <a href="https://twitter.com/m4xl1n/status/171596254864871424" data-datetime="2012-02-20T14:05:09+00:00">February 20, 2012</a></blockquote>
.span4
<blockquote class="twitter-tweet"><p><a href="https://twitter.com/search/%2523edtech">#edtech</a> <a href="https://twitter.com/search/%2523mathchat">#mathchat</a> Collaborative LaTeX document editing (like Google docs, but for LaTeX!) <a href="https://t.co/XXwnl7SA" title="https://www.sharelatex.com/">sharelatex.com</a></p>&mdash; Joe DiNoto (@mathteacher1729) <a href="https://twitter.com/mathteacher1729/status/185889900615303168" data-datetime="2012-03-31T00:43:00+00:00">March 31, 2012</a></blockquote>
<script src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
- locals.supressDefaultJs = true
script(data-main=jsPath+'main.js', src='js/libs/require.js', baseurl=jsPath)
@@ -1,15 +0,0 @@
.container
#logoArea
.span8
h1.logo.large
a(href='/').plain ShareLaTeX
span.sub.logo Online LaTeX Editor
.span3#homePagePills
ul.nav.nav-pills
li
a(href='/') Home
li
a(href='/blog') Blog
li
a(href='/comments') Comments
-64
View File
@@ -1,64 +0,0 @@
link(rel='canonical', href='https://www.sharelatex.com')
extends ../layout
block content
.masthead
.container
.row
.title.span8
h1 Online LaTeX Editor
h2 Quickly start using LaTeX and work together in real-time
div
img(src="/img/screenshot.png")
.register.span4
h2 Register now
.messageArea
include ../general/partial/registerForm
.universities
.container
.row
p Used by students and academics at:
#slides
.clearfix
.span3
img(src="/img/crests/harvard.gif", alt="harvard university logo")
.span3
img(src="/img/crests/mit.gif", alt="mit university logo")
.span3
img(src="/img/crests/oxford.gif", alt="oxford university logo")
.span3
img(src="/img/crests/tokyo.png", alt="tokyo university logo")
.clearfix
.span3
img(src="/img/crests/cambridge.png", alt="cambridge university logo")
.span3
img(src="/img/crests/liverpool.jpg", alt="liverpool university logo")
.span3
img(src="/img/crests/icl.png", alt="icl university logo")
.span3
img(src="/img/crests/yale.png", alt="yale university logo")
.clearfix
.span3
img(src="/img/crests/durham.png", alt="durham university logo")
.span3
img(src="/img/crests/nasa.png", alt="nasa university logo")
.span3
img(src="/img/crests/toronto.gif", alt="toronto university logo")
.span3
img(src="/img/crests/stanford.png", alt="stanford university logo")
.container
include ../general/long-form-features
.row
.span12.lower-signup-button
a(href="/user/subscription/plans").btn.btn-success.btn-huge Sign up now!
include ../general/social-footer
include ../general/small-footer
@@ -1,7 +0,0 @@
.span2.offset3
include ../referal/tweet
.span2
include ../referal/googleplus
.span1
include ../referal/facebookLike
+1 -1
View File
@@ -28,7 +28,7 @@ html(itemscope, itemtype='http://schema.org/Product')
ga('send', 'pageview');
- else
script(type='text/javascript')
window.ga = function() {};
window.ga = function() { console.log("Sending to GA", arguments) };
script
window.csrfToken = "#{csrfToken}";
+3 -2
View File
@@ -36,8 +36,9 @@
ul.dropdown-menu
li
a(href='/user/settings').userSettingsLink User Settings
li
a(href='/user/subscription').subscriptionLink Subscription
- if (settings.enableSubscriptions)
li
a(href='/user/subscription').subscriptionLink Subscription
li
a(href='/logout').logoutLink Logout
-else
+6 -1
View File
@@ -41,6 +41,11 @@ block content
.title
a(href='#link-modal', data-toggle="modal").link Link to us from your website
.row
.span4.offset4.bonus-banner
h2.direct-link Direct Link
.well #{buildReferalUrl("d")}
.row.ab-bonus
.span6.offset3
p.thanks When someone starts using ShareLaTeX after your recommendation we'll give you some <strong>free stuff</strong> to say thanks! Check your progress below.
@@ -103,7 +108,7 @@ block content
method: 'feed',
redirect_uri: 'https://www.sharelatex.com',
link: '!{buildReferalUrl("fb")}',
picture: 'https://www.sharelatex.com/img/logo/logosmall.png',
picture: 'https://www.sharelatex.com/brand/logo/logosmall.png',
name: 'ShareLaTeX - Online LaTeX Editor',
caption: 'Free Unlimited Projects and Compiles',
description: 'ShareLaTeX is a free online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and auto-complete'
@@ -11,7 +11,7 @@ script(type='text/javascript')
method: 'feed',
redirect_uri: 'https://www.sharelatex.com',
link: '#{buildReferalUrl()}',
picture: 'https://www.sharelatex.com/img/logo/logosmall.png',
picture: 'https://www.sharelatex.com/brand/logo/logosmall.png',
name: 'ShareLaTeX - Online LaTeX Editor',
caption: 'Free Unlimited Projects and Compiles',
description: 'ShareLaTeX is a free Online LaTeX Editor. Real time collaboration like Google Docs, with Dropbox, history and Auto Complete'
-59
View File
@@ -1,59 +0,0 @@
extends layout
block content
.container
.row
.span12.span-box
.page-header
h1 LaTeX Resources
small &nbsp; We are the LaTeX Editor, here are some other LaTeX resources we like
div
ul
li
a(href='http://en.wikibooks.org/wiki/LaTeX') LaTeX Wikibook
span &nbsp; - Covering 95% of what you need to know about LaTeX in a clear and simple way with great examples you can often copy and paste
li
a(href='http://detexify.kirelabs.org/classify.html') Detexify
span &nbsp; - A great way of finding LaTeX symbols
li
a(href='http://www.tug.dk/FontCatalogue/seriffonts.html') LaTeX Fonts
span &nbsp; - A collection of LaTeX fonts
li
a(href='http://mathurl.com/') MathUrl
span &nbsp; - allows for live equation editing
li
a(href='http://webdemo.visionobjects.com/equation.html') WebEquation
span &nbsp; - draw the symbol on the screen to see the LaTeX equivalent
li
a(href='http://www.texample.net/') TeXample.net
span &nbsp; - a great site for tikz reference
li
a(href='http://www.howtotex.com/') howtoTeX.com
span &nbsp; - a useful collection of templates, tutorials and how-tos
li
a(href='http://truben.no/latex/table/') LaTeX table editor
span &nbsp; - if you struggle with tables in LaTeX this tool gives you a nice 'excel like' view to help you along
li
a(href="http://www.math.binghamton.edu/erik/beameruserguide.pdf") Beamer user guide
span &nbsp; - A good guide into beamer
div
h3 Mobile Apps
ul
li
a(href='https://play.google.com/store/apps/details?id=coolcherrytrees.software.detexify&feature=search_result#?t=W251bGwsMSwxLDEsImNvb2xjaGVycnl0cmVlcy5zb2Z0d2FyZS5kZXRleGlmeSJd') Android
li
a(href='https://itunes.apple.com/app/detexify/id328805329?mt=8') Detexify for ios
li
a(href='http://www.windowsphone.com/en-us/store/app/detexify/3e6813bd-04b1-455b-bc14-14dfe904c54b') Detexify for Windows Phone
li
a(href='http://www.windowsphone.com/en-us/store/app/repotex/694085e0-e825-425b-a40d-40bce5cecb3b') Repotex
li
a(href='http://www.windowsphone.com/en-us/store/app/fasttexdraw/3e1b4255-7798-45ec-b679-a43e74e82769') FastTeXDraw for windows phone
hr
div
| If you know of other resources, please
a.js-tender-widget(href='#') recommend
&nbsp; them to us.
include ./general/small-footer
@@ -15,6 +15,7 @@ block content
thead
tr
th
input(type="checkbox").select-all
th email
th Name
th Registered
@@ -23,7 +24,7 @@ block content
-each user in users
tr
td
input(type="checkbox")
input(type="checkbox").select-one
td #{user.email}
td #{user.first_name} #{user.last_name}
td #{!user.holdingAccount}
+41 -26
View File
@@ -68,6 +68,7 @@
input.rename.js-rename
.dropdown-caret
i.icon-chevron-down
.entity-label.label.label-success
script(type="text/template")#folderTemplate
.entity-list-item(class="entity-{{ type }}", entity-type="{{ type }}", id="{{ id }}")
@@ -80,6 +81,7 @@
input.rename.js-rename
.dropdown-caret
i.icon-chevron-down
.entity-label.label.label-success
script(type="text/template")#entityListTemplate
.contents
@@ -180,7 +182,11 @@
button#downloadPdf.btn Download
button#downloadLinksButton.btn.dropdown-toggle(data-toggle="dropdown")
span.caret
ul.dropdown-menu#downloadLinks
ul.dropdown-menu
#downloadLinks
li.divider
li.delete-cached-files
a(href="#") Clear cached files
.btn-group.pull-right(data-toggle="buttons-radio")
button(type="button", title="Flat view")#flatViewButton.btn
i.icon-flatview
@@ -286,32 +292,31 @@
script(type="text/template")#publishProjectTemplate
-if(session && session.user && session.user.isAdmin)
.box
.page-header
h2 Publish project as template
#publishedAsTemplateArea.show-when-published.alert.alert-success
p
.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
i.icon-ok
| Your project is currently published.
a#templateLink(href='{{canonicalUrl}}') View in template gallery.
p
| Lastest version: {{publishedDate}}.
.box
.page-header
h2 Publish project as template
#publishedAsTemplateArea.show-when-published.alert.alert-success
p
.btn.btn-warning#unPublishProjectAsTemplate.pull-right Unpublish
i.icon-ok
| Your project is currently published.
a#templateLink(href='{{canonicalUrl}}') View in template gallery.
p
| Lastest version: {{publishedDate}}.
#problemWithPublishingArea
p There is a problem with our publishing service, please try again in a few minutes.
#publishWorkingArea
p Working...
div.show-when-published.show-when-unpublished.project-description
label(for="project-description") Description
.row-fluid
textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
#unpublishedAsTemplateArea.show-when-unpublished
.btn.btn-success#publishProjectAsTemplate Publish
p.show-when-published
button.btn.btn-success#republishProjectAsTemplate Re-Publish
#problemWithPublishingArea
p There is a problem with our publishing service, please try again in a few minutes.
#publishWorkingArea
p Working...
div.show-when-published.show-when-unpublished.project-description
label(for="project-description") Description
.row-fluid
textarea(placeholder="Template description", name="project-description").span12#projectDescription {{description}}
#unpublishedAsTemplateArea.show-when-unpublished
.btn.btn-success#publishProjectAsTemplate Publish
p.show-when-published
button.btn.btn-success#republishProjectAsTemplate Re-Publish
script(type="text/template")#settingsPanelTemplate
@@ -425,6 +430,10 @@
div
a(href="#", title='Show Hot Keys List')#hotkeysLink Hot keys
script(type='text/template')#DebugLinkTemplate
div
a(href="#", title='Show Debug Information')#debugLink Debug
script(type='text/template')#trackChangesPanelTemplate
#trackChangesPanel
.track-changes-side-bar
@@ -434,6 +443,12 @@
i.icon-remove
.change-list-area
.track-changes-diff
.track-changes-upgrade-popup(style="display: none;")
.message.show-when-owner
p You need to upgrade your plan to use the History feature.
button.btn.btn-primary.start-free-trial Start free trial
.message.show-when-not-owner
p Please ask the project owner to upgrade to use the History feature.
script(type='text/template')#trackChangesDiffTemplate
.track-changes-diff-toolbar.btn-toolbar
+11
View File
@@ -0,0 +1,11 @@
var keys = require('./app/js/infrastructure/Keys');
var settings = require('settings-sharelatex');
var queueName = process.argv[2];
var projectQueueName = process.argv[3];
var queue = require('fairy').connect(settings.redis.web).queue(queueName);
console.log("cleaning up queue "+ queueName + " " + projectQueueName);
queue._requeue_group(projectQueueName);
//fairy should kill the process but just in case
thirtySeconds = 30 * 1000
setTimeout(process.exit, thirtySeconds)
+15 -19
View File
@@ -5,40 +5,36 @@
"public": "./public"
},
"dependencies": {
"heapdump": "0.1.0",
"express": "3.3.4",
"mongoose": "3.6.19",
"mongoose": "3.8.8",
"mongojs": "0.10.1",
"jade": "0.28.1",
"validator": "0.4.22",
"underscore": "1.4.4",
"node-fs": "0.1.5",
"rimraf": "2.1.2",
"underscore": "1.6.0",
"rimraf": "2.2.6",
"connect-redis": "1.4.5",
"redis": "0.8.4",
"request": "2.14.0",
"redis": "0.10.1",
"request": "2.34.0",
"xml2js": "0.2.0",
"dateformat": "1.0.4-1.2.3",
"optimist": "0.3.5",
"async": "0.2.9",
"lynx": "0.0.11",
"optimist": "0.6.1",
"async": "0.6.2",
"lynx": "0.1.1",
"session.socket.io": "0.1.4",
"socket.io": "0.9.15",
"mimelib": "0.2.8",
"bufferedstream": "1.4.1",
"mixpanel": "0.0.18",
"socket.io": "0.9.16",
"mimelib": "0.2.14",
"bufferedstream": "1.6.0",
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#master",
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
"translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
"soa-req-id": "git+https://github.com/sharelatex/soa-req-id.git#master",
"fairy": "0.0.2",
"node-uuid": "1.4.0",
"mongojs": "0.9.8",
"node-uuid": "1.4.1",
"nodemailer": "0.6.1",
"bcrypt": "0.7.5",
"archiver": "0.5.1",
"nodetime": "0.8.15",
"mocha": "1.17.1",
"redback": "0.3.7"
"redback": "0.4.0",
"sanitizer": "0.1.1"
},
"devDependencies": {
"chai": "",
@@ -7,12 +7,12 @@ require [
tableRowTemplate = '''
<tr>
<td> <input type="checkbox"></td>
<td> <input type="checkbox" class="select-one"></td>
<td> {{ email }} </td>
<td> {{ first_name }} {{ last_name }} </td>
<td> {{ !holdingAccount }} </td>
<td>
<input type="hidden" name="user_id" value="{{user_id}}" class="user_id">
<input type="hidden" name="user_id" value="{{_id}}" class="user_id">
</td>
</tr>
'''
@@ -58,16 +58,19 @@ require [
$form.find("input").val('')
removeUsers = (e)->
selectedUserRows = $('td input:checked').closest('tr').find(".user_id")
selectedUserRows.each (index, userRow)->
user_id = $(userRow).val()
$.ajax
url: "/subscription/group/user/#{user_id}"
type: 'DELETE'
data:
_csrf: csrfToken
success: ->
$(userRow).parents("tr").fadeOut(250)
selectedUserRows = $('td input.select-one:checked').closest('tr').find(".user_id").toArray()
do deleteNext = () ->
row = selectedUserRows.pop()
if row?
user_id = $(row).val()
$.ajax
url: "/subscription/group/user/#{user_id}"
type: 'DELETE'
data:
_csrf: csrfToken
success: ->
$(row).parents("tr").fadeOut(250)
deleteNext()
$form.on 'keypress', (e)->
if(e.keyCode == 13)
@@ -76,3 +79,10 @@ require [
$form.find(".addUser").on 'click', addUser
$('#deleteUsers').on 'click', removeUsers
$('input.select-all').on "change", () ->
if $(@).is(":checked")
$("input.select-one").prop( "checked", true )
else
$("input.select-one").prop( "checked", false )
@@ -25,8 +25,19 @@ define [
},{
text: "Enter Billing Information"
class: "btn-primary"
callback: () ->
window.location = "/user/subscription/new?planCode=student_free_trial"
callback: () =>
options.onUpgrade?()
@gotoSubscriptionsPage()
}]
gotoSubscriptionsPage: () ->
window.open("/user/subscription/new?planCode=student_free_trial")
Modal.createModal
title: "Please refresh"
message: "Please refresh this page after starting your free trial. This will make sure all of your features are enabled."
buttons: [{
text: "OK"
class: ""
}]
showUpgradeDialog: (ide, options = {}) ->
@@ -1,4 +1,6 @@
define () ->
define [
"libs/md5"
], () ->
class AnalyticsManager
constructor: (@ide) ->
@ide.editor.on "update:doc", () =>
@@ -14,3 +16,18 @@ define () ->
ga('send', 'event', 'editor-interaction', 'single-compile')
if @compileCount == 3
ga('send', 'event', 'editor-interaction', 'multi-compile')
getABTestBucket: (test_name, buckets = []) ->
hash = CryptoJS.MD5("#{@ide.user.get("id")}:#{test_name}")
bucketIndex = parseInt(hash.toString().slice(0,2), 16) % buckets.length
return buckets[bucketIndex]
startABTest: (test_name, buckets = []) ->
value = @getABTestBucket(test_name, buckets)
ga('send', 'event', 'ab_tests', test_name, "viewed-#{value}")
return value
endABTest: (test_name, buckets = []) ->
value = @getABTestBucket(test_name, buckets)
ga('send', 'event', 'ab_tests', test_name, "converted-#{value}")
return value
@@ -2,10 +2,12 @@ define [
"auto-complete/SuggestionManager"
"auto-complete/Snippets"
"ace/autocomplete/util"
"ace/autocomplete"
"ace/range"
"ace/ext/language_tools"
], (SuggestionManager, Snippets, Util) ->
], (SuggestionManager, Snippets, Util, AutoComplete) ->
Range = require("ace/range").Range
Autocomplete = AutoComplete.Autocomplete
Util.retrievePrecedingIdentifier = (text, pos, regex) ->
currentLineOffset = 0
@@ -38,6 +40,20 @@ define [
@aceEditor.completers = [@suggestionManager, SnippetCompleter]
insertMatch = Autocomplete::insertMatch
editor = @aceEditor
Autocomplete::insertMatch = (data) ->
pos = editor.getCursorPosition()
range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
nextChar = editor.session.getTextRange(range)
# If we are in \begin{it|}, then we need to remove the trailing }
# since it will be adding in with the autocomplete of \begin{item}...
if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
editor.session.remove(range)
insertMatch.call editor.completer, data
@bindToEditorEvents()
bindToEditorEvents: () ->
@@ -0,0 +1,32 @@
define [
"utils/Modal"
], (Modal) ->
class DebugManager
template: $("#DebugLinkTemplate").html()
constructor: (@ide) ->
@$el = $(@template)
$("#toolbar-footer").append(@$el)
@$el.on "click", (e) =>
e.preventDefault()
@showDebugModal()
showDebugModal: () ->
useragent = navigator.userAgent
server_id = document.cookie.match(/SERVERID=([^;]*)/)?[1]
transport = @ide.socket.socket.transport.name
new Modal(
title: "Debug info"
message: """
Please give this information to the ShareLaTeX team:
<p><pre>
user-agent: #{useragent}
server-id: #{server_id}
transport: #{transport}
</pre></p>
"""
buttons: [
text: "OK"
]
)
@@ -81,6 +81,17 @@ define [
delete @_joinCallbacks
_onUpdateApplied: (update) ->
@ide.pushEvent "received-update",
doc_id: @doc_id
remote_doc_id: update?.doc
wantToBeJoined: @wantToBeJoined
update: update
if Math.random() < (@ide.disconnectRate or 0)
console.log "Simulating disconnect"
@ide.connectionManager.disconnect()
return
if update?.doc == @doc_id and @doc?
@doc.processUpdateFromServer update
@@ -93,6 +104,8 @@ define [
@doc?.updateConnectionState "disconnected"
_onReconnect: () ->
@ide.pushEvent "reconnected:afterJoinProject"
@connected = true
if @wantToBeJoined or @doc?.hasBufferedOps()
@_joinDoc (error) =>
@@ -137,10 +150,37 @@ define [
_bindToShareJsDocEvents: () ->
@doc.on "error", (error, meta) => @_onError error, meta
@doc.on "externalUpdate", () => @trigger "externalUpdate"
@doc.on "remoteop", () => @trigger "remoteop"
@doc.on "op:sent", () => @trigger "op:sent"
@doc.on "op:acknowledged", () => @trigger "op:acknowledged"
@doc.on "externalUpdate", () =>
@ide.pushEvent "externalUpdate",
doc_id: @doc_id
@trigger "externalUpdate"
@doc.on "remoteop", () =>
@ide.pushEvent "remoteop",
doc_id: @doc_id
@trigger "remoteop"
@doc.on "op:sent", (op) =>
@ide.pushEvent "op:sent",
doc_id: @doc_id
op: op
@trigger "op:sent"
@doc.on "op:acknowledged", (op) =>
@ide.pushEvent "op:acknowledged",
doc_id: @doc_id
op: op
@trigger "op:acknowledged"
@doc.on "op:timeout", (op) =>
@ide.pushEvent "op:timeout",
doc_id: @doc_id
op: op
@trigger "op:timeout"
ga?('send', 'event', 'error', "op timeout", "Op was now acknowledged - #{ide.socket.socket.transport.name}" )
@ide.connectionManager.reconnectImmediately()
@doc.on "flush", (inflightOp, pendingOp, version) =>
@ide.pushEvent "flush",
doc_id: @doc_id,
inflightOp: inflightOp,
pendingOp: pendingOp
v: version
_onError: (error, meta = {}) ->
console.error "ShareJS error", error, meta
@@ -71,7 +71,7 @@ define [
_saveSplitterState: () ->
if $("#editorSplitter").is(":visible")
state = $("#editorSplitter").layout().readState()
eastWidth = state.east.size
eastWidth = state.east.size + $("#editorSplitter .ui-layout-resizer-east").width()
percentWidth = eastWidth / $("#editorSplitter").width() * 100 + "%"
state.east.size = percentWidth
$.localStorage("layout.editor", state)
@@ -98,7 +98,7 @@ define [
v: update.v
op_sent_at: new Date()
timer = setTimeout () =>
@_handleError new Error("Doc op was not acknowledged in time"), meta
@trigger "op:timeout", update
, @INFLIGHT_OP_TIMEOUT
@_doc.inflightCallbacks.push () =>
clearTimeout timer
@@ -109,11 +109,16 @@ define [
_bindToDocChanges: (doc) ->
submitOp = doc.submitOp
doc.submitOp = (args...) =>
@trigger "op:sent"
@trigger "op:sent", args...
doc.pendingCallbacks.push () =>
@trigger "op:acknowledged"
@trigger "op:acknowledged", args...
submitOp.apply(doc, args)
flush = doc.flush
doc.flush = (args...) =>
@trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
flush.apply(doc, args)
_.extend(ShareJsDoc::, Backbone.Events)
return ShareJsDoc
@@ -15,6 +15,7 @@ define [
events: () ->
events = {}
events["click ##{@model.id} > .js-clickable"] = "parentOnClick"
events["click ##{@model.id} > .entity-label"] = "parentOnClick"
events["click .dropdown-caret"] = "showContextMenuFromCaret"
events["contextmenu"] = "showContextMenuFromRightClick"
return events
@@ -29,6 +30,7 @@ define [
@$nameEl = @$(".name")
@$inputEl = @$("input.js-rename")
@$entityListItemEl = @$el.children(".entity-list-item")
@$labelEl = @$entityListItemEl.children(".entity-label")
_makeEditable: () ->
if @ide.isAllowedToDoIt "readAndWrite"
@@ -48,6 +50,17 @@ define [
@$nameEl.hide()
@$inputEl.show()
setLabels: (labels) ->
label = labels[@model.get("id")]
if label?
@$entityListItemEl.addClass("show-label")
@$labelEl.text("±")
return true
else
@$entityListItemEl.removeClass("show-label")
@$labelEl.text("")
return false
select: () ->
@selected = true
@$entityListItemEl.addClass("selected")
@@ -75,6 +75,7 @@ define [
children.add(entity)
openDoc: (doc, line) ->
return if !doc?
doc_id = doc.id or doc
@trigger "open:doc", doc_id, line: line
@selectEntity(doc_id)
@@ -158,6 +159,7 @@ define [
renameEntity: (entity, name) ->
name = name?.trim()
@ide.socket.emit 'renameEntity', entity.id, entity.get("type"), name
entity.set("name", name)
@@ -196,7 +198,7 @@ define [
el = $($("#newEntityModalTemplate").html())
input = el.find("input")
create = _.once () =>
name = input.val()
name = input.val()?.trim()
if name != ""
callback(name)
modal = new Modal
@@ -277,6 +279,5 @@ define [
entity.collection?.remove(entity)
delete @views[entity_id]
setLabels: (labels) ->
@view.setLabels(labels)
@@ -19,4 +19,7 @@ define [
@rootFolderView = new RootFolderView(model: rootFolder, manager: @manager)
entities.append(@rootFolderView.$el)
@rootFolderView.render()
setLabels: (labels) ->
@rootFolderView.setLabels(labels)
@@ -107,11 +107,13 @@ define [
@$contents.hide()
@$toggle.find(".js-open").hide()
@$toggle.find(".js-closed").show()
@$entityListItemEl.removeClass("folder-open")
showEntries: () ->
@$contents.show()
@$toggle.find(".js-open").show()
@$toggle.find(".js-closed").hide()
@$entityListItemEl.addClass("folder-open")
onToggle: (e) ->
e.preventDefault()
@@ -146,3 +148,17 @@ define [
@manager.showUploadFileModal(@model)
}]
setLabels: (labels) ->
showLabel = false
for entity in @views
if entity.setLabels(labels)
showLabel = true
if showLabel
@$entityListItemEl.addClass("show-label")
@$labelEl.text("±")
return true
else
@$entityListItemEl.removeClass("show-label")
@$labelEl.text("")
return false
+14 -7
View File
@@ -26,6 +26,7 @@ define [
"tour/IdeTour"
"analytics/AnalyticsManager"
"track-changes/TrackChangesManager"
"debug/DebugManager"
"ace/ace"
"libs/jquery.color"
"libs/jquery-layout"
@@ -59,6 +60,7 @@ define [
IdeTour,
AnalyticsManager,
TrackChangesManager
DebugManager
) ->
@@ -117,10 +119,10 @@ define [
@cursorManager = new CursorManager(@)
@fileViewManager = new FileViewManager(@)
@analyticsManager = new AnalyticsManager(@)
if @userSettings.trackChanges
@trackChangesManager = new TrackChangesManager(@)
else
if @userSettings.oldHistory
@historyManager = new HistoryManager(@)
else
@trackChangesManager = new TrackChangesManager(@)
@setLoadingMessage("Connecting")
firstConnect = true
@@ -159,14 +161,18 @@ define [
buttons: [ text: "OK" ]
}
recentEvents: []
pushEvent: (type, meta = {}) ->
@recentEvents.push type: type, meta: meta, date: new Date()
if @recentEvents.length > 40
@recentEvents.shift()
reportError: (error, meta = {}) ->
meta.client_id = @socket?.socket?.sessionid
meta.transport = @socket?.socket?.transport?.name
meta.client_now = new Date()
meta.last_connected = @connectionManager.lastConnected
meta.second_last_connected = @connectionManager.secondLastConnected
meta.last_disconnected = @connectionManager.lastDisconnected
meta.second_last_disconnected = @connectionManager.secondLastDisconnected
meta.recent_events = @recentEvents
errorObj = {}
for key in Object.getOwnPropertyNames(error)
errorObj[key] = error[key]
@@ -194,6 +200,7 @@ define [
ide.hotkeysManager = new HotkeysManager ide
ide.layoutManager.resizeAllSplitters()
ide.tourManager = new IdeTour ide
ide.debugManager = new DebugManager(ide)
ide.savingAreaManager =
$savingArea : $('#saving-area')
@@ -8,15 +8,13 @@ define [
@socket = @ide.socket
@socket.on "connect", () =>
@connected = true
@secondLastConnected = @lastConnected
@lastConnected = new Date()
@ide.pushEvent("connected")
@hideModal()
@cancelReconnect()
@socket.on 'disconnect', () =>
@connected = false
@secondLastDisconnected = @lastDisconnected
@lastDisconnected = new Date()
@ide.pushEvent("disconnected")
@ide.trigger "disconnect"
setTimeout(=>
ga('send', 'event', 'editor-interaction', 'disconnect')
@@ -35,6 +33,13 @@ define [
e.preventDefault()
@tryReconnect()
@hideModal()
reconnectImmediately: () ->
@disconnect()
@tryReconnect()
disconnect: () ->
@socket.disconnect()
showModalAndStartAutoReconnect: () ->
@hideModal()
+1 -1
View File
@@ -144,7 +144,7 @@ require [
$confirm.click (e) =>
$confirm.attr("disabled", true)
$confirm.text("Creating...")
projectName = $modal.find('input').val()
projectName = $modal.find('input').val()?.trim()
$.ajax
url: '/project/new'
type:'POST'
@@ -51,6 +51,7 @@ define [
"click #splitViewButton": ->
$.localStorage("layout.pdf", "split")
@options.manager.switchToSplitView()
"click .delete-cached-files > a": -> @options.manager.deleteCachedFiles()
initialize: (@options) ->
@ide = @options.ide
@@ -90,7 +91,7 @@ define [
logButtonHtml = "Logs"
if compileErrors?
for error in compileErrors.all
for error in compileErrors.errors.concat(compileErrors.warnings).concat(compileErrors.typesetting)
errorView = new LatexErrorView(@options.manager.ide, error)
errorView.render()
@errorViews.push(errorView)
@@ -187,3 +187,26 @@ define [
downloadPdf: () ->
@ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/output/output.pdf?popupDownload=true"
deleteCachedFiles: () ->
modal = new Modal
title: "Clear cache?"
message: "This will clear all hidden LaTeX files like .aux, .bbl, etc, from our compile server. You generally don't need to do this unless you're having trouble with references. Your project files will not be deleted or changed."
buttons: [{
text: "Cancel"
}, {
text: "Clear from cache",
class: "btn-primary",
close: false
callback: ($button) =>
$button.text("Clearing...")
$button.prop("disabled", true)
$.ajax({
url: "/project/#{@ide.project_id}/output"
type: "DELETE"
headers:
"X-CSRF-Token": window.csrfToken
complete: () -> modal.remove()
})
}]
@@ -25,10 +25,11 @@ define [
@publishProjectView?.refreshPublishStatus()
setupPublish = _.once =>
@publishProjectView = new PublishProjectView
ide: @ide
el: $("#publishProject")
@publishProjectView.render()
if @ide.security? and @ide.security.permissionsLevel == "owner"
@publishProjectView = new PublishProjectView
ide: @ide
el: $("#publishProject")
@publishProjectView.render()
setupArea()
if @ide?
@@ -76,17 +77,18 @@ define [
@ide.socket.emit "removeUserFromProject", member.id
addMember: (email, privileges) ->
console.log "Adding member", email
@ide.socket.emit "addUserToProject", email, privileges, (error, added) =>
if error?
@ide.showGenericServerErrorMessage()
return
if !added
console.log "got response", error, added
ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "projectMemebrs")
AccountManager.askToUpgrade @ide,
why: "to add additional collaborators"
onUpgrade: () =>
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "projectMemebrs")
@addMember(email, privileges)
afterMemberRemoved: (memberId) ->
for member in @members.models
@@ -17,11 +17,11 @@ define [
@tab.empty()
if !@ide.isAllowedToDoIt "owner"
else if !@project.get('features').dropbox
ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "dropdown")
ga('send', 'event', 'subscription-funnel', 'askToUpgrade', "dropbox")
accountManager.askToUpgrade @ide,
onUpgrade: =>
@checkIfUserIsLinkedToDropbox()
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "dropdown")
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "dropbox")
else
@checkIfUserIsLinkedToDropbox()
@@ -126,7 +126,10 @@ define [
# http://stackoverflow.com/questions/6692031/check-if-event-is-triggered-by-a-human
if e.originalEvent?
if @ide.isAllowedToDoIt "readAndWrite"
@project.set("name", e.target.value)
newName = e.target.value?.trim()
$("input.projectName").val(newName)
@project.set("name", newName)
bindToCompiler: ->
$('select#compilers').val(@project.get("compiler"))
@@ -40,6 +40,9 @@ define [
overflow: "scroll"
this
remove: () ->
@undelegateEvents()
addItem: (model) ->
index = @collection.indexOf(model)
previousModel = @collection.models[index - 1]
@@ -208,6 +211,17 @@ define [
else
@$el.addClass("first-in-day")
@$(".change-selector-from").tooltip({
title: "Show back to this change",
placement: "left",
animation: false
})
@$(".change-selector-to").tooltip({
title: "Show up to this change",
placement: "left",
animation: false
})
return this
onClick: (e) ->
@@ -3,10 +3,11 @@ define [
"track-changes/models/Diff"
"track-changes/ChangeListView"
"track-changes/DiffView"
"account/AccountManager"
"utils/Modal"
"models/Doc"
"moment"
], (ChangeList, Diff, ChangeListView, DiffView, Modal, Doc, moment) ->
], (ChangeList, Diff, ChangeListView, DiffView, AccountManager, Modal, Doc, moment) ->
class TrackChangesManager
template: $("#trackChangesPanelTemplate").html()
@@ -39,23 +40,30 @@ define [
bindToFileTreeEvents: () ->
@ide.fileTreeManager.on "open:doc", (doc_id) =>
@doc_id = doc_id
if @enabled
@doc_id = doc_id
@updateDiff()
AB_BUCKETS: ["control", "one-week", "pop-up"]
show: () ->
@changes = new ChangeList([], project_id: @project_id, ide: @ide)
if @changeListView?
@changeListView.remove()
@changeListView = new ChangeListView(
collection : @changes,
el : @$el.find(".change-list-area")
el: @$el.find(".change-list-area")
collection: @changes
)
@changeListView.render()
@changeListView.loadUntilFull (error) =>
@autoSelectDiff()
@changeListView.on "change_diff", (fromIndex, toIndex) =>
@selectDocAndUpdateDiff(fromIndex, toIndex)
@findDocsInChange(fromIndex, toIndex)
@updateLabels()
@updateDiff()
@showUpgradeView()
if @diffView?
@diffView.remove()
@@ -65,12 +73,26 @@ define [
@ide.fileViewManager.disable()
@enable()
showUpgradeView: () ->
@$el.find("button.start-free-trial").off "click.track-changes"
@$el.find("button.start-free-trial").on "click.track-changes", () => @gotoFreeTrial()
if !@ide.project.get("features").versioning
ga('send', 'event', 'subscription-funnel', 'askToUgrade', "trackchanges")
@$el.find(".track-changes-upgrade-popup").show()
if @ide.project.get("owner") == @ide.user
@$el.find(".show-when-not-owner").hide()
else
@$el.find(".show-when-owner").hide()
hide: () ->
@ide.editor.enable()
@ide.fileViewManager.enable()
@disable()
@ide.fileTreeManager.openDoc(@doc_id)
@ide.tabManager.show "code"
@resetLabels()
autoSelectDiff: () ->
if @changes.models.length == 0
@@ -92,17 +114,27 @@ define [
@changeListView.setSelectionRange(fromIndex, 0)
@updateDiff()
selectDocAndUpdateDiff: (fromIndex, toIndex) ->
doc_ids = []
findDocsInChange: (fromIndex, toIndex) ->
@changed_doc_ids = []
for change in @changes.models.slice(toIndex, fromIndex + 1)
for doc in change.get("docs") or []
doc_ids.push doc.id if doc.id not in doc_ids
@changed_doc_ids.push doc.id if doc.id not in @changed_doc_ids
if !@doc_id? or @doc_id not in doc_ids
@doc_id = doc_ids[0]
if !@doc_id? or @doc_id not in @changed_doc_ids
@doc_id = @changed_doc_ids[0]
@updateDiff()
updateLabels: () ->
labels = {}
for doc_id in @changed_doc_ids
labels[doc_id] = true
@ide.fileTreeManager.setLabels(labels)
resetLabels: () ->
@ide.fileTreeManager.setLabels({})
updateDiff: () ->
fromIndex = @changeListView.selectedFromIndex
toIndex = @changeListView.selectedToIndex
@@ -191,4 +223,8 @@ define [
disable: () ->
@enabled = false
gotoFreeTrial: () ->
AccountManager.gotoSubscriptionsPage()
ga('send', 'event', 'subscription-funnel', 'upgraded-free-trial', "trackchanges")
return TrackChangesManager
@@ -445,6 +445,7 @@ var RenderingStates = {
var idCounter = 0;
/**
* The view for a single page.
*/
@@ -477,16 +478,30 @@ PageView.prototype = {
// Only change the width/height property of the canvas if it really
// changed. Every assignment to the width/height property clears the
// content of the canvas.
var outputScale = this.getOutputScale();
var scaledWidth = (Math.floor(viewport.width) * outputScale.sx) | 0;
var scaledHeight = (Math.floor(viewport.height) * outputScale.sy) | 0;
var newWidth = Math.floor(viewport.width);
var newHeight = Math.floor(viewport.height);
if (this.canvas.width !== newWidth) {
this.canvas.width = newWidth;
this.canvas.width = scaledWidth;
this.canvas.style.width = newWidth + 'px';
this.resetRenderState();
}
if (this.canvas.height !== newHeight) {
this.canvas.height = newHeight;
this.canvas.height = scaledHeight;
this.canvas.style.height = newHeight + 'px';
this.resetRenderState();
}
if(outputScale.scaled){
var ctx = this.getCanvasContext()
ctx.scale(outputScale.sx, outputScale.sy)
}
this.width = viewport.width;
this.height = viewport.height;
@@ -523,6 +538,28 @@ PageView.prototype = {
return this.canvas.getContext('2d');
},
/**
* Returns scale factor for the canvas. It makes sense for the HiDPI displays.
* @return {Object} The object with horizontal (sx) and vertical (sy)
scales. The scaled property is set to false if scaling is
not required, true otherwise.
*/
getOutputScale: function(){
var ctx = this.getCanvasContext()
var devicePixelRatio = window.devicePixelRatio || 1;
var backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
var pixelRatio = devicePixelRatio / backingStoreRatio;
return {
sx: pixelRatio,
sy: pixelRatio,
scaled: pixelRatio != 1
};
},
createNewCanvas: function() {
if (this.canvas) {
this.dom.removeChild(this.canvas);
@@ -78,6 +78,7 @@
font-size: 20px;
line-height: 28px;
margin-bottom: 10px;
margin-top: 16px;
}
.bonus-banner {
@@ -121,6 +122,8 @@
background-repeat: no-repeat;
background-position: 16px center;
}
h2.direct-link {
}
}
p.thanks {
@@ -467,6 +467,9 @@ body.editor {
.dropdown-caret {
display: none;
}
.entity-label {
display: none;
}
}
.entity-folder {
@@ -527,6 +530,33 @@ body.editor {
}
}
}
.entity-list-item.show-label {
.dropdown-caret {
display: none;
}
.entity-label {
display: block;
position: absolute;
top: 3px;
right: 3px;
font-size: 13px;
line-height: 13px;
padding: 2px 6px 3px;
background-color: hsl(100, 80%, 42%);
font-weight: normal;
text-shadow: none;
&:hover {
background-color: hsl(100, 80%, 35%)
}
}
}
.entity-list-item.folder-open.show-label {
.entity-label {
display: none;
}
}
li img, .entity-list-item i {
margin: 4px;
@@ -79,6 +79,39 @@
}
}
.track-changes-upgrade-control, .track-changes-upgrade-popup {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
}
.track-changes-upgrade-popup {
background-color: rgba(128,128,128,0.4);
.message {
margin: auto;
margin-top: 200px;
padding: 10px 10px 14px 10px;
width: 400px;
font-weight: bold;
text-align: center;
background-color: white;
.border-radius(8px);
}
}
.track-changes-upgrade-control {
background-color: #eeeeee;
text-align: center;
.message {
font-size: 18px;
margin: 12px;
margin-top: 36px;
}
}
.deleted-change-background,
.deleted-change-foreground,
.inserted-change-background,
@@ -193,6 +226,10 @@
}
li.loading-changes, li.empty-message {
padding: 6px;
cursor: default;
&:hover {
background-color: inherit;
}
}
li.selected-change {
background-color: #eaeaea;
@@ -242,6 +279,11 @@
}
}
}
li.track-changes-upgrade-oneweek {
padding: 15px;
background-color: rgb(255, 251, 210);
cursor: default;
}
}
ul.change-list.hover-state {
li {
@@ -256,6 +298,7 @@
li.hover-selected {
.change-selectors {
.range {
top: 0;
background-color: #999;
}
}
@@ -122,6 +122,37 @@ describe "AuthenticationController", ->
it "should only redirect to the local path", ->
expect(@res.body).to.deep.equal redir: "/test"
describe "getLoggedInUserId", ->
beforeEach ->
@req =
session :{}
it "should return the user id from the session", (done)->
@user_id = "2134"
@req.session.user =
_id:@user_id
@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
expect(user_id).to.equal @user_id
done()
it "should return an error if there is no user on the session", (done)->
@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
expect(err).to.exist
done()
it "should return an error if there is no session", (done)->
@req = {}
@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
expect(err).to.exist
done()
it "should return an error if there is no req", (done)->
@req = {}
@AuthenticationController.getLoggedInUserId @req, (err, user_id)=>
expect(err).to.exist
done()
describe "getLoggedInUser", ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)
@@ -16,7 +16,7 @@ describe "CompileController", ->
apis:
clsi:
url: "clsi.example.com"
"request": @request = {}
"request": @request = sinon.stub()
"../../models/Project": Project: @Project = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"../../infrastructure/Metrics": @Metrics = { inc: sinon.stub() }
@@ -68,15 +68,23 @@ describe "CompileController", ->
describe "proxyToClsi", ->
beforeEach ->
@request.get = sinon.stub().returns(@proxy = {
@request.returns(@proxy = {
pipe: sinon.stub()
on: sinon.stub()
})
@upstream =
statusCode: 204
headers: { "mock": "header" }
@req.method = "mock-method"
@CompileController.proxyToClsi(@url = "/test", @req, @res, @next)
it "should open a request to the CLSI", ->
@request.get
.calledWith(url: "#{@settings.apis.clsi.url}#{@url}", timeout: 60 * 1000)
@request
.calledWith(
method: @req.method
url: "#{@settings.apis.clsi.url}#{@url}",
timeout: 60 * 1000
)
.should.equal true
it "should pass the request on to the client", ->
@@ -87,6 +95,17 @@ describe "CompileController", ->
it "should bind an error handle to the request proxy", ->
@proxy.on.calledWith("error").should.equal true
describe "deleteAuxFiles", ->
beforeEach ->
@CompileController.proxyToClsi = sinon.stub()
@req.params =
Project_id: @project_id
@CompileController.deleteAuxFiles @req, @res, @next
it "should proxy to the CLSI", ->
@CompileController.proxyToClsi
.calledWith("/project/#{@project_id}", @req, @res, @next)
.should.equal true
describe "compileAndDownloadPdf", ->
beforeEach ->
@@ -609,3 +609,114 @@ describe "EditorController", ->
done()
describe "deleteProject", ->
beforeEach ->
@err = "errro"
@ProjectHandler::deleteProject = sinon.stub().callsArgWith(1, @err)
it "should call the project handler", (done)->
@EditorController.deleteProject @project_id, (err)=>
err.should.equal @err
@ProjectHandler::deleteProject.calledWith(@project_id).should.equal true
done()
describe "renameEntity", ->
beforeEach ->
@err = "errro"
@entity_id = "entity_id_here"
@entityType = "doc"
@newName = "bobsfile.tex"
@ProjectHandler::renameEntity = sinon.stub().callsArgWith(4, @err)
@EditorRealTimeController.emitToRoom = sinon.stub()
it "should call the project handler", (done)->
@EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, =>
@ProjectHandler::renameEntity.calledWith(@project_id, @entity_id, @entityType, @newName).should.equal true
done()
it "should emit the update to the room", (done)->
@EditorController.renameEntity @project_id, @entity_id, @entityType, @newName, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'reciveEntityRename', @entity_id, @newName).should.equal true
done()
describe "moveEntity", ->
beforeEach ->
@err = "errro"
@entity_id = "entity_id_here"
@entityType = "doc"
@folder_id = "313dasd21dasdsa"
@ProjectEntityHandler.moveEntity = sinon.stub().callsArgWith(4, @err)
@EditorRealTimeController.emitToRoom = sinon.stub()
it "should call the ProjectEntityHandler", (done)->
@EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, =>
@ProjectEntityHandler.moveEntity.calledWith(@project_id, @entity_id, @folder_id, @entityType).should.equal true
done()
it "should emit the update to the room", (done)->
@EditorController.moveEntity @project_id, @entity_id, @folder_id, @entityType, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'reciveEntityMove', @entity_id, @folder_id).should.equal true
done()
describe "renameProject", ->
beforeEach ->
@err = "errro"
@window_id = "kdsjklj290jlk"
@newName = "new name here"
@ProjectHandler::renameProject = sinon.stub().callsArgWith(3, @err)
@EditorRealTimeController.emitToRoom = sinon.stub()
it "should call the ProjectHandler", (done)->
@EditorController.renameProject @project_id, @window_id, @newName, =>
@ProjectHandler::renameProject.calledWith(@project_id, @window_id, @newName).should.equal true
done()
it "should emit the update to the room", (done)->
@EditorController.renameProject @project_id, @window_id, @newName, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'projectNameUpdated', @window_id, @newName).should.equal true
done()
describe "setPublicAccessLevel", ->
beforeEach ->
@err = "errro"
@newAccessLevel = "public"
@ProjectHandler::setPublicAccessLevel = sinon.stub().callsArgWith(2, @err)
@EditorRealTimeController.emitToRoom = sinon.stub()
it "should call the ProjectHandler", (done)->
@EditorController.setPublicAccessLevel @project_id, @newAccessLevel, =>
@ProjectHandler::setPublicAccessLevel.calledWith(@project_id, @newAccessLevel).should.equal true
done()
it "should emit the update to the room", (done)->
@EditorController.setPublicAccessLevel @project_id, @newAccessLevel, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'publicAccessLevelUpdated', @newAccessLevel).should.equal true
done()
describe "setRootDoc", ->
beforeEach ->
@err = "errro"
@newRootDocID = "21312321321"
@ProjectEntityHandler.setRootDoc = sinon.stub().callsArgWith(2, @err)
@EditorRealTimeController.emitToRoom = sinon.stub()
it "should call the ProjectEntityHandler", (done)->
@EditorController.setRootDoc @project_id, @newRootDocID, =>
@ProjectEntityHandler.setRootDoc.calledWith(@project_id, @newRootDocID).should.equal true
done()
it "should emit the update to the room", (done)->
@EditorController.setRootDoc @project_id, @newRootDocID, =>
@EditorRealTimeController.emitToRoom.calledWith(@project_id, 'rootDocUpdated', @newRootDocID).should.equal true
done()
@@ -8,36 +8,41 @@ require('chai').should()
describe 'Project details handler', ->
beforeEach ->
@ProjectGetter =
getProjectWithoutDocLines: sinon.stub()
@ProjectModel =
update: sinon.stub()
@handler = SandboxedModule.require modulePath, requires:
"./ProjectGetter":@ProjectGetter
'../../models/Project': Project:@ProjectModel
'logger-sharelatex':
log:->
err:->
@project_id = "321l3j1kjkjl"
@user_id = "user-id-123"
@project =
name: "project"
description: "this is a great project"
something:"should not exist"
compiler: "latexxxxxx"
owner_ref: @user_id
@user =
features: "mock-features"
@ProjectGetter =
getProjectWithoutDocLines: sinon.stub().callsArgWith(1, null, @project)
@ProjectModel =
update: sinon.stub()
@UserGetter =
getUser: sinon.stub().callsArgWith(1, null, @user)
@handler = SandboxedModule.require modulePath, requires:
"./ProjectGetter":@ProjectGetter
'../../models/Project': Project:@ProjectModel
"../User/UserGetter": @UserGetter
'logger-sharelatex':
log:->
err:->
describe "getDetails", ->
it "should find the project", (done)->
@ProjectGetter.getProjectWithoutDocLines.callsArgWith(1, null, @project)
@handler.getDetails @project_id, (err, details)=>
it "should find the project and owner", (done)->
@handler.getDetails @project_id, (err, details)=>
details.name.should.equal @project.name
details.description.should.equal @project.description
details.compiler.should.equal @project.compiler
details.features.should.equal @user.features
assert.equal(details.something, undefined)
done()
it "should return the error", (done)->
error = "some error"
@ProjectGetter.getProjectWithoutDocLines.callsArgWith(1, error)
@@ -5,6 +5,7 @@ expect = chai.expect
modulePath = "../../../../app/js/Features/Project/ProjectGetter.js"
SandboxedModule = require('sandboxed-module')
ObjectId = require("mongojs").ObjectId
assert = require("chai").assert
describe "ProjectGetter", ->
beforeEach ->
@@ -57,7 +58,8 @@ describe "ProjectGetter", ->
@db.users.find = (query, callback) =>
callback null, [@user_lookup[query._id.toString()]]
sinon.spy @db.users, "find"
@ProjectGetter.populateProjectWithUsers @project, @callback
@ProjectGetter.populateProjectWithUsers @project, (err, project)=>
@callback err, project
it "should look up each user", ->
for user in @users
@@ -73,5 +75,5 @@ describe "ProjectGetter", ->
expect(@project.collaberator_refs).to.deep.equal [@users[3], @users[4]]
it "should call the callback", ->
@callback.calledWith(null, @project).should.equal true
assert.deepEqual @callback.args[0][1], @project
@@ -76,11 +76,13 @@ describe "Subscription Group Handler", ->
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
it "should locate the subscription", (done)->
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
@SubscriptionLocator.getUsersSubscription.calledWith(@adminUser_id).should.equal true
done()
it "should get the users by id", (done)->
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
@subscription.member_ids = ["1234", "342432", "312312"]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
@UserLocator.findById.calledWith(@subscription.member_ids[0]).should.equal true
@@ -88,3 +90,13 @@ describe "Subscription Group Handler", ->
@UserLocator.findById.calledWith(@subscription.member_ids[2]).should.equal true
users.length.should.equal @subscription.member_ids.length
done()
it "should just return the id if the user can not be found as they may have deleted their account", (done)->
@UserLocator.findById.callsArgWith(1)
@subscription.member_ids = ["1234", "342432", "312312"]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
assert.deepEqual users[0], {_id:@subscription.member_ids[0]}
assert.deepEqual users[1], {_id:@subscription.member_ids[1]}
assert.deepEqual users[2], {_id:@subscription.member_ids[2]}
done()
@@ -32,7 +32,7 @@ describe 'third party data store', ->
@poller._sendToTpds = sinon.stub().callsArgWith(1, null)
@poller._markPollHappened = sinon.stub()
@poller.pollUsersWithDropbox (err)=>
@userModel.find.calledWith({"dropbox.access_token.oauth_token_secret":{"$exists":true}}, "_id").should.equal true
@userModel.find.calledWith({"dropbox.access_token.oauth_token_secret":{"$exists":true}, "features.dropbox":true}, "_id").should.equal true
@poller._sendToTpds.calledWith([users[0]._id, users[1]._id, users[2]._id,]).should.equal true
@poller._markPollHappened.called.should.equal true
done()
@@ -25,7 +25,7 @@ describe "UserDeleter", ->
cancelSubscription: sinon.stub().callsArgWith(1)
@UserDeleter = SandboxedModule.require modulePath, requires:
"../../models/User": User: @User
"../../managers/NewsletterManager": @NewsletterManager
"../Newsletter/NewsletterManager": @NewsletterManager
"../Subscription/SubscriptionHandler": @SubscriptionHandler
"../Project/ProjectDeleter": @ProjectDeleter
@@ -108,20 +108,15 @@ describe 'AutomaticSnapshotManager', ->
callback(null, @project_ids)
else
throw new Error("unexpected key: #{key}")
sinon.stub(@AutomaticSnapshotManager, "takeSnapshotIfRequired")
.callsArgWith(1)
@AutomaticSnapshotManager.takeAutomaticSnapshots(@callback)
sinon.stub(@AutomaticSnapshotManager, "takeSnapshotIfRequired").callsArgWith(1, null)
@AutomaticSnapshotManager.takeAutomaticSnapshots @callback
afterEach ->
@AutomaticSnapshotManager.takeSnapshotIfRequired.restore()
it "should call takeSnapshotIfRequired for each project id", ->
for project_id in @project_ids
@AutomaticSnapshotManager.takeSnapshotIfRequired.calledWith(project_id)
.should.equal true
it "should call the callback", ->
@callback.calledWith(null).should.equal true
@callback.calledWith(undefined).should.equal true
describe "removing project from marked set", ->
@@ -1,18 +1,23 @@
chai = require("chai")
chai.should()
expect = chai.expect
request = require "request"
Settings = require "settings-sharelatex"
# Monkey patch request cookies, because the new tough-cookie module
# assumes it's not a secure cookie if the url is not HTTPS
request = require "request"
jar = request.jar()
jar.getCookieString = (uri) ->
return @_jar.getCookieStringSync uri, secure: true
request = request.defaults jar: jar
port = Settings.internal?.web?.port or Settings.port or 3000
buildUrl = (path) -> "http://localhost:#{port}/#{path}"
describe "Opening", ->
before (done) ->
@jar = request.jar()
request.get {
url: buildUrl("register")
jar: @jar
headers:
"X-Forwarded-Proto": "https"
}, (error, response, body) =>
@@ -23,7 +28,6 @@ describe "Opening", ->
email: Settings.smokeTest.user
password: Settings.smokeTest.password
_csrf: csrf
jar: @jar
headers:
"X-Forwarded-Proto": "https"
}, (error, response, body) ->
@@ -33,7 +37,6 @@ describe "Opening", ->
it "a project", (done) ->
request {
url: buildUrl("project/#{Settings.smokeTest.projectId}")
jar: @jar
headers:
"X-Forwarded-Proto": "https"
}, (error, response, body) ->
@@ -47,12 +50,11 @@ describe "Opening", ->
it "the project list", (done) ->
request {
url: buildUrl("project")
jar: @jar
headers:
"X-Forwarded-Proto": "https"
}, (error, response, body) ->
expect(error, "smoke test: error returned in getting project list").to.not.exist
expect(response.statusCode, "smoke test: response code is not 200 getting project life").to.equal(200)
expect(response.statusCode, "smoke test: response code is not 200 getting project list").to.equal(200)
expect(!!body.match("<title>Your Projects - Online LaTeX Editor ShareLaTeX</title>"), "smoke test: body does not have correct title").to.equal true
expect(!!body.match("<h1>Projects</h1>"), "smoke test: body does not have correct h1").to.equal true
done()