Merge remote-tracking branch 'origin/master' into afc-email-tokens

This commit is contained in:
Alberto Fernández Capel 2018-06-01 16:49:47 +01:00
commit 25d7196570
67 changed files with 1207 additions and 413 deletions

View file

@ -1,5 +1,5 @@
BetaProgramHandler = require './BetaProgramHandler'
UserLocator = require "../User/UserLocator"
UserGetter = require "../User/UserGetter"
Settings = require "settings-sharelatex"
logger = require 'logger-sharelatex'
AuthenticationController = require '../Authentication/AuthenticationController'
@ -30,7 +30,7 @@ module.exports = BetaProgramController =
optInPage: (req, res, next)->
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {user_id}, "showing beta participation page for user"
UserLocator.findById user_id, (err, user)->
UserGetter.getUser user_id, (err, user)->
if err
logger.err {err, user_id}, "error fetching user"
return next(err)

View file

@ -27,7 +27,7 @@ module.exports = CollaboratorsInviteController =
_checkShouldInviteEmail: (email, callback=(err, shouldAllowInvite)->) ->
if Settings.restrictInvitesToExistingAccounts == true
logger.log {email}, "checking if user exists with this email"
UserGetter.getUser {email: email}, {_id: 1}, (err, user) ->
UserGetter.getUserByMainEmail email, {_id: 1}, (err, user) ->
return callback(err) if err?
userExists = user? and user?._id?
callback(null, userExists)

View file

@ -32,7 +32,7 @@ module.exports = CollaboratorsInviteHandler =
_trySendInviteNotification: (projectId, sendingUser, invite, callback=(err)->) ->
email = invite.email
UserGetter.getUser {email: email}, {_id: 1}, (err, existingUser) ->
UserGetter.getUserByMainEmail email, {_id: 1}, (err, existingUser) ->
if err?
logger.err {projectId, email}, "error checking if user exists"
return callback(err)

View file

@ -5,7 +5,8 @@ logger = require 'logger-sharelatex'
module.exports = LinkedFilesController = {
Agents: {
url: require('./UrlAgent')
url: require('./UrlAgent'),
project_file: require('./ProjectFileAgent')
}
createLinkedFile: (req, res, next) ->
@ -22,11 +23,17 @@ module.exports = LinkedFilesController = {
linkedFileData = Agent.sanitizeData(data)
linkedFileData.provider = provider
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
if error?
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
return Agent.handleError(error, req, res, next)
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error) ->
return next(error) if error?
res.send(204) # created
}
Agent.checkAuth project_id, linkedFileData, user_id, (err, allowed) ->
return Agent.handleError(err, req, res, next) if err?
return res.sendStatus(403) if !allowed
Agent.decorateLinkedFileData linkedFileData, (err, newLinkedFileData) ->
return Agent.handleError(err) if err?
linkedFileData = newLinkedFileData
Agent.writeIncomingFileToDisk project_id, linkedFileData, user_id, (error, fsPath) ->
if error?
logger.error {err: error, project_id, name, linkedFileData, parent_folder_id, user_id}, 'error writing linked file to disk'
return Agent.handleError(error, req, res, next)
EditorController.upsertFile project_id, parent_folder_id, name, fsPath, linkedFileData, "upload", user_id, (error, file) ->
return next(error) if error?
res.json(new_file_id: file._id) # created
}

View file

@ -0,0 +1,127 @@
FileWriter = require('../../infrastructure/FileWriter')
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectLocator = require('../Project/ProjectLocator')
ProjectGetter = require('../Project/ProjectGetter')
DocstoreManager = require('../Docstore/DocstoreManager')
FileStoreHandler = require('../FileStore/FileStoreHandler')
FileWriter = require('../../infrastructure/FileWriter')
_ = require "underscore"
Settings = require 'settings-sharelatex'
AccessDeniedError = (message) ->
error = new Error(message)
error.name = 'AccessDenied'
error.__proto__ = AccessDeniedError.prototype
return error
AccessDeniedError.prototype.__proto__ = Error.prototype
BadEntityTypeError = (message) ->
error = new Error(message)
error.name = 'BadEntityType'
error.__proto__ = BadEntityTypeError.prototype
return error
BadEntityTypeError.prototype.__proto__ = Error.prototype
BadDataError = (message) ->
error = new Error(message)
error.name = 'BadData'
error.__proto__ = BadDataError.prototype
return error
BadDataError.prototype.__proto__ = Error.prototype
ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'ProjectNotFound'
error.__proto__ = ProjectNotFoundError.prototype
return error
ProjectNotFoundError.prototype.__proto__ = Error.prototype
SourceFileNotFoundError = (message) ->
error = new Error(message)
error.name = 'BadData'
error.__proto__ = SourceFileNotFoundError.prototype
return error
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
module.exports = ProjectFileAgent =
sanitizeData: (data) ->
return _.pick(
data,
'source_project_id',
'source_entity_path'
)
_validate: (data) ->
return (
data.source_project_id? &&
data.source_entity_path?
)
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
callback = _.once(callback)
{ source_project_id } = data
return callback(new BadDataError()) if !source_project_id?
ProjectGetter.getProject source_project_id, (err, project) ->
return callback(err) if err?
return callback(new ProjectNotFoundError()) if !project?
callback(err, _.extend(data, {source_project_display_name: project.name}))
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
{source_project_id, source_entity_path} = data
AuthorizationManager.canUserReadProject current_user_id, source_project_id, null, (err, canRead) ->
return callback(err) if err?
callback(null, canRead)
writeIncomingFileToDisk:
(project_id, data, current_user_id, callback = (error, fsPath) ->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
{source_project_id, source_entity_path} = data
ProjectLocator.findElementByPath {
project_id: source_project_id,
path: source_entity_path
}, (err, entity, type) ->
if err?
if err.toString().match(/^not found.*/)
err = new SourceFileNotFoundError()
return callback(err)
ProjectFileAgent._writeEntityToDisk source_project_id, entity._id, type, callback
_writeEntityToDisk: (project_id, entity_id, type, callback=(err, location)->) ->
callback = _.once(callback)
if type == 'doc'
DocstoreManager.getDoc project_id, entity_id, (err, lines) ->
return callback(err) if err?
FileWriter.writeLinesToDisk entity_id, lines, callback
else if type == 'file'
FileStoreHandler.getFileStream project_id, entity_id, null, (err, fileStream) ->
return callback(err) if err?
FileWriter.writeStreamToDisk entity_id, fileStream, callback
else
callback(new BadEntityTypeError())
handleError: (error, req, res, next) ->
if error instanceof AccessDeniedError
res.status(403).send("You do not have access to this project")
else if error instanceof BadDataError
res.status(400).send("The submitted data is not valid")
else if error instanceof BadEntityTypeError
res.status(400).send("The file is the wrong type")
else if error instanceof SourceFileNotFoundError
res.status(404).send("Source file not found")
else if error instanceof ProjectNotFoundError
res.status(404).send("Project not found")
else
next(error)
next()

View file

@ -27,6 +27,12 @@ module.exports = UrlAgent = {
url: @._prependHttpIfNeeded(data.url)
}
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
return callback(null, data)
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
callback(null, true)
writeIncomingFileToDisk: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
callback = _.once(callback)
url = data.url
@ -65,4 +71,4 @@ module.exports = UrlAgent = {
if !Settings.apis?.linkedUrlProxy?.url?
throw new Error('no linked url proxy configured')
return "#{Settings.apis.linkedUrlProxy.url}?url=#{encodeURIComponent(url)}"
}
}

View file

@ -9,7 +9,7 @@ logger = require("logger-sharelatex")
module.exports =
generateAndEmailResetToken:(email, callback = (error, exists) ->)->
UserGetter.getUser email:email, (err, user)->
UserGetter.getUserByMainEmail email, (err, user)->
if err then return callback(err)
if !user? or user.holdingAccount
logger.err email:email, "user could not be found for password reset"

View file

@ -25,6 +25,7 @@ Sources = require "../Authorization/Sources"
TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
Modules = require '../../infrastructure/Modules'
ProjectEntityHandler = require './ProjectEntityHandler'
crypto = require 'crypto'
module.exports = ProjectController =
@ -138,6 +139,33 @@ module.exports = ProjectController =
return next(err) if err?
res.sendStatus 200
userProjectsJson: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
ProjectGetter.findAllUsersProjects user_id,
'name lastUpdated publicAccesLevel archived owner_ref tokens', (err, projects) ->
return next(err) if err?
projects = ProjectController._buildProjectList(projects)
.filter((p) -> !p.archived)
.filter((p) -> !p.isV1Project)
.map((p) -> {_id: p.id, name: p.name, accessLevel: p.accessLevel})
res.json({projects: projects})
projectEntitiesJson: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
project_id = req.params.Project_id
ProjectGetter.getProject project_id, (err, project) ->
return next(err) if err?
ProjectEntityHandler.getAllEntitiesFromProject project, (err, docs, files) ->
return next(err) if err?
entities = docs.concat(files)
.sort (a, b) -> a.path > b.path # Sort by path ascending
.map (e) -> {
path: e.path,
type: if e.doc? then 'doc' else 'file'
}
res.json({project_id: project_id, entities: entities})
projectListPage: (req, res, next)->
timer = new metrics.Timer("project-list")
user_id = AuthenticationController.getLoggedInUserId(req)
@ -313,6 +341,7 @@ module.exports = ProjectController =
maxDocLength: Settings.max_doc_length
useV2History: !!project.overleaf?.history?.display
showRichText: req.query?.rt == 'true'
showTestControls: req.query?.tc == 'true' || user.isAdmin
showPublishModal: req.query?.pm == 'true'
timer.done()

View file

@ -45,37 +45,40 @@ wrapWithLock = (methodWithoutLock) ->
methodWithLock
module.exports = ProjectEntityUpdateHandler = self =
# this doesn't need any locking because it's only called by ProjectDuplicator
copyFileFromExistingProjectWithProject: (project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
projectHistoryId = project.overleaf?.history?.id
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
return callback(err) if err?
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id)=>
if !origonalFileRef?
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
return callback()
# convert any invalid characters in original file to '_'
fileRef = new File name : SafePath.clean(origonalFileRef.name)
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
return callback(err)
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
if err?
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
return callback(err)
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
copyFileFromExistingProjectWithProject: wrapWithLock
beforeLock: (next) ->
(project, folder_id, originalProject_id, origonalFileRef, userId, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
logger.log { project_id, folder_id, originalProject_id, origonalFileRef }, "copying file in s3 with project"
ProjectEntityMongoUpdateHandler._confirmFolder project, folder_id, (folder_id) ->
if !origonalFileRef?
logger.err { project_id, folder_id, originalProject_id, origonalFileRef }, "file trying to copy is null"
return callback()
# convert any invalid characters in original file to '_'
fileRef = new File name : SafePath.clean(origonalFileRef.name)
FileStoreHandler.copyFile originalProject_id, origonalFileRef._id, project._id, fileRef._id, (err, fileStoreUrl)->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error coping file in s3"
return callback(err)
next(project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback)
withLock: (project, folder_id, originalProject_id, origonalFileRef, userId, fileRef, fileStoreUrl, callback = (error, fileRef, folder_id) ->)->
project_id = project._id
projectHistoryId = project.overleaf?.history?.id
ProjectEntityMongoUpdateHandler._putElement project, folder_id, fileRef, "file", (err, result, newProject) ->
if err?
logger.err { err, project_id, folder_id }, "error putting element as part of copy"
return callback(err)
TpdsUpdateSender.addFile { project_id, file_id:fileRef._id, path:result?.path?.fileSystem, rev:fileRef.rev, project_name:project.name}, (err) ->
if err?
logger.err { err, project_id, folder_id, originalProject_id, origonalFileRef }, "error sending file to tpds worker"
newFiles = [
file: fileRef
path: result?.path?.fileSystem
url: fileStoreUrl
]
DocumentUpdaterHandler.updateProjectStructure project_id, projectHistoryId, userId, {newFiles, newProject}, (error) ->
return callback(error) if error?
callback null, fileRef, folder_id
updateDocLines: (project_id, doc_id, lines, version, ranges, callback = (error) ->)->
ProjectGetter.getProjectWithoutDocLines project_id, (err, project)->

View file

@ -10,6 +10,8 @@ Async = require('async')
oneMinInMs = 60 * 1000
fiveMinsInMs = oneMinInMs * 5
if !settings.apis?.references?.url?
logger.log "references search not enabled"
module.exports = ReferencesHandler =

View file

@ -2,7 +2,7 @@ async = require("async")
_ = require("underscore")
SubscriptionUpdater = require("./SubscriptionUpdater")
SubscriptionLocator = require("./SubscriptionLocator")
UserLocator = require("../User/UserLocator")
UserGetter = require("../User/UserGetter")
LimitationsManager = require("./LimitationsManager")
logger = require("logger-sharelatex")
OneTimeTokenHandler = require("../Security/OneTimeTokenHandler")
@ -22,7 +22,7 @@ module.exports = SubscriptionGroupHandler =
if limitReached
logger.err adminUserId:adminUserId, newEmail:newEmail, "group subscription limit reached not adding user to group"
return callback(limitReached:limitReached)
UserLocator.findByEmail newEmail, (err, user)->
UserGetter.getUserByMainEmail newEmail, (err, user)->
return callback(err) if err?
if user?
SubscriptionUpdater.addUserToGroup adminUserId, user._id, (err)->
@ -52,7 +52,7 @@ module.exports = SubscriptionGroupHandler =
jobs = _.map subscription.member_ids, (user_id)->
return (cb)->
UserLocator.findById user_id, (err, user)->
UserGetter.getUser user_id, (err, user)->
if err? or !user?
users.push _id:user_id
return cb()

View file

@ -0,0 +1,80 @@
path = require('path')
Project = require('../../../js/models/Project').Project
ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager')
ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler')
AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController')
settings = require('settings-sharelatex')
fs = require('fs')
request = require('request')
uuid = require('uuid')
logger = require('logger-sharelatex')
async = require("async")
module.exports = TemplatesController =
getV1Template: (req, res)->
templateVersionId = req.params.Template_version_id
templateId = req.query.id
if !/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)
logger.err templateVersionId:templateVersionId, templateId: templateId, "invalid template id or version"
return res.sendStatus 400
data = {}
data.templateVersionId = templateVersionId
data.templateId = templateId
data.name = req.query.templateName
data.compiler = req.query.latexEngine
res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data
createProjectFromV1Template: (req, res)->
currentUserId = AuthenticationController.getLoggedInUserId(req)
zipUrl = "#{settings.apis.v1.url}/api/v1/sharelatex/templates/#{req.body.templateVersionId}"
zipReq = request(zipUrl, {
'auth': {
'user': settings.apis.v1.user,
'pass': settings.apis.v1.pass
}
})
TemplatesController.createFromZip(
zipReq,
{
templateName: req.body.templateName,
currentUserId: currentUserId,
compiler: req.body.compiler
docId: req.body.docId
templateId: req.body.templateId
templateVersionId: req.body.templateVersionId
},
req,
res
)
createFromZip: (zipReq, options, req, res)->
dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}"
writeStream = fs.createWriteStream(dumpPath)
zipReq.on "error", (error) ->
logger.error err: error, "error getting zip from template API"
zipReq.pipe(writeStream)
writeStream.on 'close', ->
ProjectUploadManager.createProjectFromZipArchive options.currentUserId, options.templateName, dumpPath, (err, project)->
if err?
logger.err err:err, zipReq:zipReq, "problem building project from zip"
return res.sendStatus 500
setCompiler project._id, options.compiler, ->
fs.unlink dumpPath, ->
delete req.session.templateData
conditions = {_id:project._id}
update = {
fromV1TemplateId:options.templateId,
fromV1TemplateVersionId:options.templateVersionId
}
Project.update conditions, update, {}, (err)->
res.redirect "/project/#{project._id}"
setCompiler = (project_id, compiler, callback)->
if compiler?
ProjectOptionsHandler.setCompiler project_id, compiler, callback
else
callback()

View file

@ -0,0 +1,8 @@
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
saveTemplateDataInSession: (req, res, next)->
if req.query.templateName
req.session.templateData = req.query
next()

View file

@ -0,0 +1,9 @@
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
module.exports =
saveTemplateDataInSession: (req, res, next)->
if req.query.templateName
req.session.templateData = req.query
next()

View file

@ -0,0 +1,10 @@
AuthenticationController = require('../Authentication/AuthenticationController')
TemplatesController = require("./TemplatesController")
TemplatesMiddlewear = require('./TemplatesMiddlewear')
module.exports =
apply: (app)->
app.get '/project/new/template/:Template_version_id', TemplatesMiddlewear.saveTemplateDataInSession, AuthenticationController.requireLogin(), TemplatesController.getV1Template
app.post '/project/new/template', AuthenticationController.requireLogin(), TemplatesController.createProjectFromV1Template

View file

@ -1,6 +1,6 @@
UserHandler = require("./UserHandler")
UserDeleter = require("./UserDeleter")
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
User = require("../../models/User").User
newsLetterManager = require('../Newsletter/NewsletterManager')
UserRegistrationHandler = require("./UserRegistrationHandler")
@ -45,7 +45,7 @@ module.exports = UserController =
unsubscribe: (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
UserLocator.findById user_id, (err, user)->
UserGetter.getUser user_id, (err, user)->
newsLetterManager.unsubscribe user, ->
res.send()

View file

@ -1,19 +1,10 @@
User = require("../../models/User").User
UserLocator = require("./UserLocator")
logger = require("logger-sharelatex")
metrics = require('metrics-sharelatex')
module.exports = UserCreator =
getUserOrCreateHoldingAccount: (email, callback = (err, user)->)->
self = @
UserLocator.findByEmail email, (err, user)->
if user?
callback(err, user)
else
self.createNewUser email:email, holdingAccount:true, callback
createNewUser: (opts, callback)->
logger.log opts:opts, "creating new user"
user = new User()

View file

@ -6,6 +6,8 @@ ObjectId = mongojs.ObjectId
module.exports = UserGetter =
getUser: (query, projection, callback = (error, user) ->) ->
if query?.email?
return callback(new Error("Don't use getUser to find user by email"), null)
if arguments.length == 2
callback = projection
projection = {}
@ -19,6 +21,13 @@ module.exports = UserGetter =
db.users.findOne query, projection, callback
getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
email = email.trim()
if arguments.length == 2
callback = projection
projection = {}
db.users.findOne email: email, projection, callback
getUsers: (user_ids, projection, callback = (error, users) ->) ->
try
user_ids = user_ids.map (u) -> ObjectId(u.toString())
@ -39,6 +48,7 @@ module.exports = UserGetter =
[
'getUser',
'getUserByMainEmail',
'getUsers',
'getUserOrUserStubById'
].map (method) ->

View file

@ -1,21 +0,0 @@
mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex")
db = mongojs.db
ObjectId = mongojs.ObjectId
logger = require('logger-sharelatex')
module.exports = UserLocator =
findByEmail: (email, callback)->
email = email.trim()
db.users.findOne email:email, (err, user)->
callback(err, user)
findById: (_id, callback)->
db.users.findOne _id:ObjectId(_id+""), callback
[
'findById',
'findByEmail'
].map (method) ->
metrics.timeAsyncMethod UserLocator, method, 'mongo.UserLocator', logger

View file

@ -1,4 +1,3 @@
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
UserSessionsManager = require("./UserSessionsManager")
ErrorController = require("../Errors/ErrorController")
@ -61,7 +60,7 @@ module.exports =
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log user: user_id, "loading settings page"
shouldAllowEditingDetails = !(Settings?.ldap?.updateUserDetailsOnLogin) and !(Settings?.saml?.updateUserDetailsOnLogin)
UserLocator.findById user_id, (err, user)->
UserGetter.getUser user_id, (err, user)->
return next(err) if err?
res.render 'user/settings',
title:'account_settings'

View file

@ -1,6 +1,7 @@
sanitize = require('sanitizer')
User = require("../../models/User").User
UserCreator = require("./UserCreator")
UserGetter = require("./UserGetter")
AuthenticationManager = require("../Authentication/AuthenticationManager")
NewsLetterManager = require("../Newsletter/NewsletterManager")
async = require("async")
@ -47,7 +48,7 @@ module.exports = UserRegistrationHandler =
if !requestIsValid
return callback(new Error("request is not valid"))
userDetails.email = userDetails.email?.trim()?.toLowerCase()
User.findOne email:userDetails.email, (err, user)->
UserGetter.getUserByMainEmail userDetails.email, (err, user) =>
if err?
return callback err
if user?.holdingAccount == false

View file

@ -3,7 +3,7 @@ mongojs = require("../../infrastructure/mongojs")
metrics = require("metrics-sharelatex")
db = mongojs.db
ObjectId = mongojs.ObjectId
UserLocator = require("./UserLocator")
UserGetter = require("./UserGetter")
module.exports = UserUpdater =
updateUser: (query, update, callback = (error) ->) ->
@ -18,7 +18,7 @@ module.exports = UserUpdater =
changeEmailAddress: (user_id, newEmail, callback)->
self = @
logger.log user_id:user_id, newEmail:newEmail, "updaing email address of user"
UserLocator.findByEmail newEmail, (error, user) ->
UserGetter.getUserByMainEmail newEmail, (error, user) ->
if user?
return callback({message:"alread_exists"})
self.updateUser user_id.toString(), {

View file

@ -6,16 +6,31 @@ Settings = require 'settings-sharelatex'
request = require 'request'
module.exports = FileWriter =
_ensureDumpFolderExists: (callback=(error)->) ->
fs.mkdir Settings.path.dumpFolder, (error) ->
if error? and error.code != 'EEXIST'
# Ignore error about already existing
return callback(error)
callback(null)
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
callback = _.once(callback)
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
FileWriter._ensureDumpFolderExists (error) ->
return callback(error) if error?
fs.writeFile fsPath, lines.join('\n'), (error) ->
return callback(error) if error?
callback(null, fsPath)
writeStreamToDisk: (identifier, stream, callback = (error, fsPath) ->) ->
callback = _.once(callback)
fsPath = "#{Settings.path.dumpFolder}/#{identifier}_#{uuid.v4()}"
stream.pause()
fs.mkdir Settings.path.dumpFolder, (error) ->
FileWriter._ensureDumpFolderExists (error) ->
return callback(error) if error?
stream.resume()
if error? and error.code != 'EEXIST'
# Ignore error about already existing
return callback(error)
writeStream = fs.createWriteStream(fsPath)
stream.pipe(writeStream)
@ -39,4 +54,4 @@ module.exports = FileWriter =
else
err = new Error("bad response from url: #{response.statusCode}")
logger.err {err, identifier, url}, err.message
callback(err)
callback(err)

View file

@ -30,7 +30,8 @@ module.exports = Modules =
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
@viewIncludes[view].push pug.compile(fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")), doctype: "html")
filePath = Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".pug")
@viewIncludes[view].push pug.compileFile(filePath, doctype: "html")
moduleIncludes: (view, locals) ->
compiledPartials = Modules.viewIncludes[view] or []

View file

@ -48,6 +48,7 @@ MetaController = require('./Features/Metadata/MetaController')
TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
Features = require('./infrastructure/Features')
LinkedFilesRouter = require './Features/LinkedFiles/LinkedFilesRouter'
TemplatesRouter = require './Features/Templates/TemplatesRouter'
logger = require("logger-sharelatex")
_ = require("underscore")
@ -80,10 +81,10 @@ module.exports = class Router
ContactRouter.apply(webRouter, privateApiRouter)
AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter)
LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter)
TemplatesRouter.apply(webRouter)
Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
if Settings.enableSubscriptions
webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus
@ -119,6 +120,11 @@ module.exports = class Router
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
privateApiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
webRouter.get '/user/projects', AuthenticationController.requireLogin(), ProjectController.userProjectsJson
webRouter.get '/project/:Project_id/entities', AuthenticationController.requireLogin(),
AuthorizationMiddlewear.ensureUserCanReadProject,
ProjectController.projectEntitiesJson
webRouter.get '/project', AuthenticationController.requireLogin(), ProjectController.projectListPage
webRouter.post '/project/new', AuthenticationController.requireLogin(), ProjectController.newProject
@ -201,7 +207,7 @@ module.exports = class Router
webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.get "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApiAndInjectUserDetails
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
webRouter.post '/project/:project_id/doc/:doc_id/restore', AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreDocFromDeletedDoc
webRouter.post "/project/:project_id/restore_file", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, HistoryController.restoreFileFromV2
privateApiRouter.post "/project/:Project_id/history/resync", AuthenticationController.httpAuth, HistoryController.resyncProjectHistory

View file

@ -1,67 +0,0 @@
script(type='text/ng-template', id='supportModalTemplate')
.modal-header
button.close(
type="button"
data-dismiss="modal"
ng-click="close()"
) ×
h3 #{translate("contact_us")}
.modal-body.contact-us-modal
form(name="contactForm")
span(ng-show="sent == false")
.alert.alert-danger(ng-show="error") Something went wrong sending your request :(
label
| #{translate("subject")}
.form-group
input.field.text.medium.span8.form-control(
name="subject",
required
ng-model="form.subject",
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
maxlength='255',
tabindex='1',
onkeyup='')
.contact-suggestions(ng-show="suggestions.length")
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>" + translate("knowledge_base") + "</a>" })}
ul.contact-suggestion-list
li(ng-repeat="suggestion in suggestions")
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
span(ng-bind-html="suggestion.name")
i.fa.fa-angle-right
label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
| #{translate("email")}
.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
input.field.text.medium.span8.form-control(
name="email",
required
ng-model="form.email",
ng-init="form.email = '"+getUserEmail()+"'",
type='email', spellcheck='false',
value='',
maxlength='255',
tabindex='2')
label#title12.desc
| #{translate("project_url")} (#{translate("optional")})
.form-group
input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
label.desc
| #{translate("contact_message_label")}
.form-group
textarea.field.text.medium.span8.form-control(
name="body",
required
ng-model="form.message",
type='text',
value='',
tabindex='4',
onkeyup=''
)
.form-group.text-center
input.btn-success.btn.btn-lg(
type='submit',
ng-disabled="contactForm.$invalid || sending",
ng-click="contactUs()"
value=translate("contact_us")
)
span(ng-show="sent")
p #{translate("request_sent_thank_you")}

View file

@ -147,7 +147,7 @@ html(itemscope, itemtype='http://schema.org/Product')
src=buildJsPath('libs/require.js', {hashedPath:true})
)
include contact-us-modal
!= moduleIncludes("contactModal", locals)
include v1-tooltip
include sentry

View file

@ -47,7 +47,18 @@ div.binary-file.full-size(
|
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
span(ng-if="openFile.linkedFileData.provider == 'url'")
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
p
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
| Imported from
|
a(ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
| {{ openFile.linkedFileData.source_project_display_name }}
| /{{ openFile.linkedFileData.source_entity_path.slice(1) }},
|
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
span(ng-if="openFile.linkedFileData.provider == 'url' || openFile.linkedFileData.provider == 'project_file'")
button.btn.btn-success(
href, ng-click="refreshFile(openFile)",
ng-disabled="refreshing"
@ -63,3 +74,7 @@ div.binary-file.full-size(
i.fa.fa-fw.fa-download
|
| #{translate("download")}
div(ng-if="refreshError")
br
.alert.alert-danger.col-md-6.col-md-offset-3
| Error: {{ refreshError}}

View file

@ -33,11 +33,11 @@ div.full-size(
i.fa.fa-arrow-left
| &nbsp;&nbsp;#{translate("open_a_file_on_the_left")}
!= moduleIncludes('editor:toolbar', locals)
!= moduleIncludes('editor:main', locals)
#editor(
ace-editor="editor",
ng-if="!editor.richText",
ng-if="!editor.showRichText",
ng-show="!!editor.sharejs_doc && !editor.opening",
style=showRichText ? "top: 32px" : "",
theme="settings.theme",
@ -73,8 +73,6 @@ div.full-size(
line-height="settings.lineHeight || ui.defaultLineHeight"
)
!= moduleIncludes('editor:body', locals)
include ./review-panel
.ui-layout-east

View file

@ -342,6 +342,77 @@ script(type='text/ng-template', id='newDocModalTemplate')
span(ng-show="state.inflight") #{translate("creating")}...
// Project Linked Files Modal
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
.modal-header
h3 New file from Project
.modal-body
div
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
div
form
.form-controls
label(for="project-select") Select a Project
span(ng-show="state.inFlight.projects")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-select"
ng-model="data.selectedProjectId"
ng-disabled="!shouldEnableProjectSelect()"
)
option(value="" disabled selected) - Please Select a Project
option(
ng-repeat="project in data.projects"
value="{{ project._id }}"
) {{ project.name }}
br
.form-controls
label(for="project-entity-select") Select a File
span(ng-show="state.inFlight.entities")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-entity-select"
ng-model="data.selectedProjectEntity"
ng-disabled="!shouldEnableProjectEntitySelect()"
)
option(value="" disabled selected) - Please Select a File
option(
ng-repeat="projectEntity in data.projectEntities"
value="{{ projectEntity.path }}"
) {{ projectEntity.path.slice(1) }}
br
.form-controls
label(for="name") File Name In This Project
input.form-control(
type="text"
placeholder="example.tex"
required
ng-model="data.name"
name="name"
)
br
.modal-footer
span(ng-show="state.inFlight.create")
i.fa.fa-spinner.fa-spin
| &nbsp;
button.btn.btn-default(
ng-disabled="state.inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="!shouldEnableCreateButton()"
ng-click="create()"
)
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='linkedFileModalTemplate')
.modal-header
h3 New file from URL

View file

@ -13,7 +13,7 @@
}"
)
| in <strong>{{history.diff.pathname}}</strong>
.toolbar-right
.toolbar-right(ng-if="permissions.write")
a.btn.btn-danger.btn-sm(
href,
ng-click="openRestoreDiffModal()"

View file

@ -62,6 +62,23 @@ aside#left-menu.full-size(
!= moduleIncludes("editorLeftMenu:editing_services", locals)
if showTestControls
h4 Test Controls
ul.list-unstyled.nav(ng-controller="TestControlsController")
li
a(href="#" ng-click="richText()")
i.fa.fa-exclamation.fa-fw
| Rich Text
li
a(href="#" ng-click="openProjectLinkedFileModal()")
i.fa.fa-exclamation.fa-fw
| Project-Linked-File Modal
li
a(href="#" ng-click="openLinkedFileModal()")
i.fa.fa-exclamation.fa-fw
| URL-Linked-File Modal
h4(ng-show="!anonymous") #{translate("settings")}
form.settings(ng-controller="SettingsController", ng-show="!anonymous")
.containter-fluid
@ -179,6 +196,7 @@ aside#left-menu.full-size(
option(value="pdfjs") #{translate("built_in")}
option(value="native") #{translate("native")}
h4 #{translate("hotkeys")}
ul.list-unstyled.nav
li(ng-controller="HotkeysController")

View file

@ -0,0 +1,26 @@
extends ../../layout
block content
script.
$(document).ready(function(){
$('#create_form').submit();
});
.editor.full-size
.loading-screen()
.loading-screen-brand-container
.loading-screen-brand(
style="height: 20%;"
)
h3.loading-screen-label() #{translate("Opening template")}
span.loading-screen-ellip .
span.loading-screen-ellip .
span.loading-screen-ellip .
form(id='create_form' method='POST' action='/project/new/template/')
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden" name="templateId" value=templateId)
input(type="hidden" name="templateVersionId" value=templateVersionId)
input(type="hidden" name="templateName" value=name)
input(type="hidden" name="compiler" value=compiler)

View file

@ -1,4 +1,7 @@
.col-xs-6
- var titleClasses = settings.overleaf ? "col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-item(
select-individual,
type="checkbox",
@ -37,8 +40,42 @@
tooltip-placement="right"
tooltip-append-to-body="true"
)
.col-xs-4
div(class=lastUpdatedClasses)
if settings.overleaf
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}
else
span.last-modified {{project.lastUpdated | formatDate}}
if settings.overleaf
.hidden-xs.col-sm-3.col-md-2.action-btn-row
button.btn.btn-link.action-btn(
tooltip=translate('copy'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="clone($event)"
)
i.icon.fa.fa-files-o
button.btn.btn-link.action-btn(
tooltip=translate('download'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="download($event)"
)
i.icon.fa.fa-cloud-download
button.btn.btn-link.action-btn(
ng-if="!project.archived"
tooltip=translate('archive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="archive($event)"
)
i.icon.fa.fa-inbox
button.btn.btn-link.action-btn(
ng-if="project.archived"
tooltip=translate('unarchive'),
tooltip-placement="top",
tooltip-append-to-body="true",
ng-click="restore($event)"
)
i.icon.fa.fa-reply

View file

@ -131,7 +131,10 @@
)
li.container-fluid
.row
.col-xs-6
- var titleClasses = settings.overleaf ? " col-xs-6 col-sm-4 col-md-6" : "col-xs-6"
- var lastUpdatedClasses = settings.overleaf ? " col-xs-4 col-sm-3 col-md-2" : "col-xs-4"
div(class=titleClasses)
input.select-all(
select-all,
type="checkbox"
@ -142,9 +145,12 @@
.col-xs-2
span.header.clickable(ng-click="changePredicate('accessLevel')") #{translate("owner")}
i.tablesort.fa(ng-class="getSortIconClass('accessLevel')")
.col-xs-4
div(class=lastUpdatedClasses)
span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")}
i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')")
if settings.overleaf
.hidden-xs.col-sm-3.col-md-2.action-btn-row-header
span.header #{translate("actions")}
li.project_entry.container-fluid(
ng-repeat="project in visibleProjects | orderBy:predicate:reverse",
ng-controller="ProjectListItemController"

View file

@ -1,4 +1,4 @@
.col-xs-6
.col-xs-6.col-sm-4.col-md-6
.select-item
span.v1-badge(
aria-label=translate("v1_badge")
@ -21,5 +21,5 @@
.col-xs-2
span.owner {{ownerName()}}
.col-xs-4
.col-xs-4.col-sm-3.col-md-2
span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") {{project.lastUpdated | fromNowDate}}

View file

@ -146,8 +146,8 @@ module.exports = settings =
url: "http://#{process.env['CONTACTS_HOST'] or 'localhost'}:3036"
sixpack:
url: ""
# references:
# url: "http://localhost:3040"
references:
url: "http://#{process.env['REFERENCES_HOST'] or 'localhost'}:3040"
notifications:
url: "http://#{process.env['NOTIFICATIONS_HOST'] or 'localhost'}:3042"
analytics:

View file

@ -17,6 +17,7 @@ services:
PROJECT_HISTORY_ENABLED: 'true'
ENABLED_LINKED_FILE_TYPES: 'url'
LINKED_URL_PROXY: 'http://localhost:6543'
ENABLED_LINKED_FILE_TYPES: 'url,project_file'
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
depends_on:
- redis

View file

@ -18,6 +18,7 @@ define [
"ide/chat/index"
"ide/clone/index"
"ide/hotkeys/index"
"ide/test-controls/index"
"ide/wordcount/index"
"ide/directives/layout"
"ide/directives/validFile"
@ -34,6 +35,7 @@ define [
"directives/videoPlayState"
"services/queued-http"
"services/validateCaptcha"
"services/wait-for"
"filters/formatDate"
"main/event"
"main/account-upgrade"
@ -54,7 +56,7 @@ define [
SafariScrollPatcher
) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata) ->
App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) ->
# Don't freak out if we're already in an apply callback
$scope.$originalApply = $scope.$apply
$scope.$apply = (fn = () ->) ->

View file

@ -2,7 +2,7 @@ define [
"base"
"moment"
], (App, moment) ->
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", ($scope, $rootScope, $http, $timeout, $element, ide) ->
App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) ->
TWO_MEGABYTES = 2 * 1024 * 1024
@ -31,6 +31,7 @@ define [
data: null
$scope.refreshing = false
$scope.refreshError = null
MAX_URL_LENGTH = 60
FRONT_OF_URL_LENGTH = 35
@ -48,9 +49,27 @@ define [
$scope.refreshFile = (file) ->
$scope.refreshing = true
$scope.refreshError = null
ide.fileTreeManager.refreshLinkedFile(file)
.then () ->
loadTextFileFilePreview()
.then (response) ->
{ data } = response
{ new_file_id } = data
$timeout(
() ->
waitFor(
() ->
ide.fileTreeManager.findEntityById(new_file_id)
5000
)
.then (newFile) ->
ide.binaryFilesManager.openFile(newFile)
.catch (err) ->
console.warn(err)
, 0
)
$scope.refreshError = null
.catch (response) ->
$scope.refreshError = response.data
.finally () ->
$scope.refreshing = false
@ -86,11 +105,9 @@ define [
# show dots when payload is closs to cutoff
if data.length >= (TWO_MEGABYTES - 200)
$scope.textPreview.shouldShowDots = true
try
# remove last partial line
data = data.replace(/\n.*$/, '')
finally
$scope.textPreview.data = data
data = data?.replace?(/\n.*$/, '')
$scope.textPreview.data = data
$timeout(setHeight, 0)
.catch (error) ->
console.error(error)

View file

@ -14,7 +14,7 @@ define [
opening: true
trackChanges: false
wantTrackChanges: false
richText: false
showRichText: false
}
@$scope.$on "entity:selected", (event, entity) =>

View file

@ -43,6 +43,19 @@ define [
}
)
$scope.openProjectLinkedFileModal = window.openProjectLinkedFileModal = () ->
unless 'project_file' in window.data.enabledLinkedFileTypes
console.warn("Project linked files are not enabled")
return
$modal.open(
templateUrl: "projectLinkedFileModalTemplate"
controller: "ProjectLinkedFileModalController"
scope: $scope
resolve: {
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
}
)
$scope.orderByFoldersFirst = (entity) ->
return '0' if entity?.type == "folder"
return '1'
@ -201,6 +214,117 @@ define [
$modalInstance.dismiss('cancel')
]
App.controller "ProjectLinkedFileModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->
$scope.data =
projects: null # or []
selectedProjectId: null
projectEntities: null # or []
selectedProjectEntity: null
name: null
$scope.state =
inFlight:
projects: false
entities: false
create: false
error: false
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
return if !newVal
$scope.data.selectedProjectEntity = null
$scope.getProjectEntities($scope.data.selectedProjectId)
# auto-set filename based on selected file
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
return if !newVal
fileName = newVal.split('/').reverse()[0]
if fileName
$scope.data.name = fileName
_setInFlight = (type) ->
$scope.state.inFlight[type] = true
_reset = (opts) ->
isError = opts.err == true
inFlight = $scope.state.inFlight
inFlight.projects = inFlight.entities = inFlight.create = false
$scope.state.error = isError
$scope.shouldEnableProjectSelect = () ->
{ state, data } = $scope
return !state.inFlight.projects && data.projects
$scope.shouldEnableProjectEntitySelect = () ->
{ state, data } = $scope
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
$scope.shouldEnableCreateButton = () ->
state = $scope.state
data = $scope.data
return !state.inFlight.projects &&
!state.inFlight.entities &&
data.projects &&
data.selectedProjectId &&
data.projectEntities &&
data.selectedProjectEntity &&
data.name
$scope.getUserProjects = () ->
_setInFlight('projects')
ide.$http.get("/user/projects", {
_csrf: window.csrfToken
})
.then (resp) ->
$scope.data.projectEntities = null
$scope.data.projects = resp.data.projects.filter (p) ->
p._id != ide.project_id
_reset(err: false)
.catch (err) ->
_reset(err: true)
$scope.getProjectEntities = (project_id) =>
_setInFlight('entities')
ide.$http.get("/project/#{project_id}/entities", {
_csrf: window.csrfToken
})
.then (resp) ->
if $scope.data.selectedProjectId == resp.data.project_id
$scope.data.projectEntities = resp.data.entities
_reset(err: false)
.catch (err) ->
_reset(err: true)
$scope.init = () ->
$scope.getUserProjects()
$timeout($scope.init, 0)
$scope.create = () ->
projectId = $scope.data.selectedProjectId
path = $scope.data.selectedProjectEntity
name = $scope.data.name
if !name || !path || !projectId
_reset(err: true)
return
_setInFlight('create')
ide.fileTreeManager
.createLinkedFile(name, parent_folder, 'project_file', {
source_project_id: projectId,
source_entity_path: path
})
.then () ->
_reset(err: false)
$modalInstance.close()
.catch (response)->
{ data } = response
_reset(err: true)
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
]
# TODO: rename all this to UrlLinkedFilModalController
App.controller "LinkedFileModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->

View file

@ -1,7 +1,7 @@
define [
"base"
], (App) ->
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking) ->
App.controller "HistoryV2DiffController", ($scope, ide, event_tracking, waitFor) ->
$scope.restoreState =
inflight: false
error: false
@ -24,17 +24,16 @@ define [
$scope.restoreState.inflight = false
openEntity = (data) ->
iterations = 0
{id, type} = data
do tryOpen = () ->
if iterations > 5
return
iterations += 1
entity = ide.fileTreeManager.findEntityById(id)
if entity? and type == 'doc'
ide.editorManager.openDoc(entity)
else if entity? and type == 'file'
ide.binaryFilesManager.openFile(entity)
else
setTimeout(tryOpen, 500)
waitFor(
() ->
ide.fileTreeManager.findEntityById(id)
3000
)
.then (entity) ->
if type == 'doc'
ide.editorManager.openDoc(entity)
else if type == 'file'
ide.binaryFilesManager.openFile(entity)
.catch (err) ->
console.warn(err)

View file

@ -0,0 +1,16 @@
define [
"base"
"ace/ace"
], (App) ->
App.controller "TestControlsController", ($scope) ->
$scope.openProjectLinkedFileModal = () ->
window.openProjectLinkedFileModal()
$scope.openLinkedFileModal = () ->
window.openLinkedFileModal()
$scope.richText = () ->
current = window.location.toString()
target = "#{current}#{if window.location.search then '&' else '?'}rt=true"
window.location.href = target

View file

@ -0,0 +1,3 @@
define [
"ide/test-controls/controllers/TestControlsController"
], () ->

View file

@ -1,79 +1,7 @@
define [
"base"
"libs/platform"
"services/algolia-search"
], (App, platform) ->
App.controller 'ContactModal', ($scope, $modal) ->
$scope.contactUsModal = () ->
modalInstance = $modal.open(
templateUrl: "supportModalTemplate"
controller: "SupportModalController"
)
App.controller 'SupportModalController', ($scope, $modalInstance, algoliaSearch, event_tracking) ->
$scope.form = {}
$scope.sent = false
$scope.sending = false
$scope.suggestions = [];
_handleSearchResults = (success, results) ->
suggestions = for hit in results.hits
page_underscored = hit.pageName.replace(/\s/g,'_')
suggestion =
url :"/learn/kb/#{page_underscored}"
name : hit._highlightResult.pageName.value
event_tracking.sendMB "contact-form-suggestions-shown" if results.hits.length
$scope.$applyAsync () ->
$scope.suggestions = suggestions
$scope.contactUs = ->
if !$scope.form.email? or $scope.form.email == ""
console.log "email not set"
return
$scope.sending = true
ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32)
message = $scope.form.message
if $scope.form.project_url?
message = "#{message}\n\n project_url = #{$scope.form.project_url}"
params =
email: $scope.form.email
message: message or ""
subject: $scope.form.subject + " - [#{ticketNumber}]"
labels: "support"
about: "<div>browser: #{platform?.name} #{platform?.version}</div>
<div>os: #{platform?.os?.family} #{platform?.os?.version}</div>"
Groove.createTicket params, (response)->
$scope.sending = false
if response.responseText == "" # Blocked request or similar
$scope.error = true
else
data = JSON.parse(response.responseText)
if data.errors?
$scope.error = true
else
$scope.sent = true
$scope.$apply()
$scope.$watch "form.subject", (newVal, oldVal) ->
if newVal and newVal != oldVal and newVal.length > 3
algoliaSearch.searchKB newVal, _handleSearchResults, {
hitsPerPage: 3
typoTolerance: 'strict'
}
else
$scope.suggestions = [];
$scope.clickSuggestionLink = (url) ->
event_tracking.sendMB "contact-form-suggestions-clicked", { url }
$scope.close = () ->
$modalInstance.close()
App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
$scope.form = {}

View file

@ -350,14 +350,15 @@ define [
$scope.archiveOrLeaveSelectedProjects()
$scope.archiveOrLeaveSelectedProjects = () ->
selected_projects = $scope.getSelectedProjects()
selected_project_ids = $scope.getSelectedProjectIds()
$scope.archiveOrLeaveProjects($scope.getSelectedProjects())
$scope.archiveOrLeaveProjects = (projects) ->
projectIds = projects.map (p) -> p.id
# Remove project from any tags
for tag in $scope.tags
$scope._removeProjectIdsFromTagArray(tag, selected_project_ids)
$scope._removeProjectIdsFromTagArray(tag, projectIds)
for project in selected_projects
for project in projects
project.tags = []
if project.accessLevel == "owner"
project.archived = true
@ -414,13 +415,14 @@ define [
$scope.updateVisibleProjects()
$scope.restoreSelectedProjects = () ->
selected_projects = $scope.getSelectedProjects()
selected_project_ids = $scope.getSelectedProjectIds()
$scope.restoreProjects($scope.getSelectedProjects())
for project in selected_projects
$scope.restoreProjects = (projects) ->
projectIds = projects.map (p) -> p.id
for project in projects
project.archived = false
for project_id in selected_project_ids
for projectId in projectIds
queuedHttp {
method: "POST"
url: "/project/#{project_id}/restore"
@ -437,13 +439,14 @@ define [
)
$scope.downloadSelectedProjects = () ->
selected_project_ids = $scope.getSelectedProjectIds()
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
if selected_project_ids.length > 1
path = "/project/download/zip?project_ids=#{selected_project_ids.join(',')}"
else
path = "/project/#{selected_project_ids[0]}/download/zip"
$scope.downloadProjectsById($scope.getSelectedProjectIds())
$scope.downloadProjectsById = (projectIds) ->
event_tracking.send 'project-list-page-interaction', 'project action', 'Download Zip'
if projectIds.length > 1
path = "/project/download/zip?project_ids=#{projectIds.join(',')}"
else
path = "/project/#{projectIds[0]}/download/zip"
window.location = path
$scope.openV1ImportModal = (project) ->
@ -490,3 +493,19 @@ define [
$scope.$watch "project.selected", (value) ->
if value?
$scope.updateSelectedProjects()
$scope.clone = (e) ->
e.stopPropagation()
$scope.cloneProject($scope.project, "#{$scope.project.name} (Copy)")
$scope.download = (e) ->
e.stopPropagation()
$scope.downloadProjectsById([$scope.project.id])
$scope.archive = (e) ->
e.stopPropagation()
$scope.archiveOrLeaveProjects([$scope.project])
$scope.restore = (e) ->
e.stopPropagation()
$scope.restoreProjects([$scope.project])

View file

@ -8,7 +8,7 @@ define [
kbIdx = client.initIndex(window.sharelatex.algolia?.indexes?.kb)
service =
searchWiki: wikiIdx.search.bind(wikiIdx)
searchKB: kbIdx.search.bind(kbIdx)
searchWiki: if wikiIdx then wikiIdx.search.bind(wikiIdx) else null
searchKB: if kbIdx then kbIdx.search.bind(kbIdx) else null
return service

View file

@ -0,0 +1,20 @@
define [
"base"
], (App) ->
App.factory "waitFor", ($q) ->
waitFor = (testFunction, timeout, pollInterval=500) ->
iterationLimit = Math.floor(timeout / pollInterval)
iterations = 0
$q(
(resolve, reject) ->
do tryIteration = () ->
if iterations > iterationLimit
return reject(new Error("waiting too long, #{JSON.stringify({timeout, pollInterval})}"))
iterations += 1
result = testFunction()
if result?
resolve(result)
else
setTimeout(tryIteration, pollInterval)
)
return waitFor

View file

@ -369,6 +369,16 @@ ul.project-list {
.v1-badge {
margin-left: -4px;
}
.action-btn-row-header, .action-btn-row {
padding-right: 20px;
text-align: right;
}
.action-btn {
padding: 0 0.3em;
margin-left: 0.2em;
}
}
i.tablesort {
padding-left: 8px;

View file

@ -145,10 +145,15 @@ output {
opacity: 1; // iOS fix for unreadable disabled content
}
// Reset height for `textarea`s
// Reset height for `textarea`s, and smaller border-radius
textarea& {
height: auto;
border-radius: @border-radius-base;
}
// Smaller border-radius for `select` inputs
select& {
border-radius: @border-radius-base;
}
}

View file

@ -27,6 +27,122 @@ describe "LinkedFiles", ->
@owner.login ->
mkdirp Settings.path.dumpFolder, done
describe "creating a project linked file", ->
before (done) ->
@source_doc_name = 'test.txt'
async.series [
(cb) =>
@owner.createProject 'plf-test-one', {template: 'blank'}, (error, project_id) =>
@project_one_id = project_id
cb(error)
(cb) =>
@owner.getProject @project_one_id, (error, project) =>
@project_one = project
@project_one_root_folder_id = project.rootFolder[0]._id.toString()
cb(error)
(cb) =>
@owner.createProject 'plf-test-two', {template: 'blank'}, (error, project_id) =>
@project_two_id = project_id
cb(error)
(cb) =>
@owner.getProject @project_two_id, (error, project) =>
@project_two = project
@project_two_root_folder_id = project.rootFolder[0]._id.toString()
cb(error)
(cb) =>
@owner.createDocInProject @project_two_id,
@project_two_root_folder_id,
@source_doc_name,
(error, doc_id) =>
@source_doc_id = doc_id
cb(error)
(cb) =>
@owner.createDocInProject @project_two_id,
@project_two_root_folder_id,
'some-harmless-doc.txt',
(error, doc_id) =>
cb(error)
], done
it 'should produce a list of the users projects', (done) ->
@owner.request.get {
url: "/user/projects",
json: true
}, (err, response, body) =>
expect(err).to.not.exist
expect(body).to.deep.equal {
projects: [
{ _id: @project_one_id, name: 'plf-test-one', accessLevel: 'owner' },
{ _id: @project_two_id, name: 'plf-test-two', accessLevel: 'owner' }
]
}
done()
it 'should produce a list of entities in the project', (done) ->
@owner.request.get {
url: "/project/#{@project_two_id}/entities",
json: true
}, (err, response, body) =>
expect(err).to.not.exist
expect(body).to.deep.equal {
project_id: @project_two_id,
entities: [
{ path: '/main.tex', type: 'doc' },
{ path: '/some-harmless-doc.txt', type: 'doc' },
{ path: '/test.txt', type: 'doc' }
]
}
done()
it 'should import a file from the source project', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
json:
name: 'test-link.txt',
parent_folder_id: @project_one_root_folder_id,
provider: 'project_file',
data:
source_project_id: @project_two_id,
source_entity_path: "/#{@source_doc_name}",
}, (error, response, body) =>
new_file_id = body.new_file_id
@existing_file_id = new_file_id
expect(new_file_id).to.exist
@owner.getProject @project_one_id, (error, project) =>
return done(error) if error?
firstFile = project.rootFolder[0].fileRefs[0]
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
expect(firstFile.linkedFileData).to.deep.equal {
provider: 'project_file',
source_project_id: @project_two_id,
source_entity_path: "/#{@source_doc_name}",
source_project_display_name: "plf-test-two"
}
expect(firstFile.name).to.equal('test-link.txt')
done()
it 'should refresh the file', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
json:
name: 'test-link.txt',
parent_folder_id: @project_one_root_folder_id,
provider: 'project_file',
data:
source_project_id: @project_two_id,
source_entity_path: "/#{@source_doc_name}",
}, (error, response, body) =>
new_file_id = body.new_file_id
expect(new_file_id).to.exist
expect(new_file_id).to.not.equal @existing_file_id
@owner.getProject @project_one_id, (error, project) =>
return done(error) if error?
firstFile = project.rootFolder[0].fileRefs[0]
expect(firstFile._id.toString()).to.equal(new_file_id.toString())
expect(firstFile.name).to.equal('test-link.txt')
done()
describe "creating a URL based linked file", ->
before (done) ->
@owner.createProject "url-linked-files-project", {template: "blank"}, (error, project_id) =>
@ -50,7 +166,7 @@ describe "LinkedFiles", ->
name: 'url-test-file-1'
}, (error, response, body) =>
throw error if error?
expect(response.statusCode).to.equal 204
expect(response.statusCode).to.equal 200
@owner.getProject @project_id, (error, project) =>
throw error if error?
file = project.rootFolder[0].fileRefs[0]
@ -76,7 +192,7 @@ describe "LinkedFiles", ->
name: 'url-test-file-2'
}, (error, response, body) =>
throw error if error?
expect(response.statusCode).to.equal 204
expect(response.statusCode).to.equal 200
@owner.request.post {
url: "/project/#{@project_id}/linked_file",
json:
@ -88,7 +204,7 @@ describe "LinkedFiles", ->
name: 'url-test-file-2'
}, (error, response, body) =>
throw error if error?
expect(response.statusCode).to.equal 204
expect(response.statusCode).to.equal 200
@owner.getProject @project_id, (error, project) =>
throw error if error?
file = project.rootFolder[0].fileRefs[1]
@ -168,7 +284,7 @@ describe "LinkedFiles", ->
name: 'url-test-file-6'
}, (error, response, body) =>
throw error if error?
expect(response.statusCode).to.equal 204
expect(response.statusCode).to.equal 200
@owner.getProject @project_id, (error, project) =>
throw error if error?
file = _.find project.rootFolder[0].fileRefs, (file) ->

View file

@ -143,6 +143,18 @@ class User
return callback(err)
callback(null)
createDocInProject: (project_id, parent_folder_id, name, callback=(error, doc_id)->) ->
@getCsrfToken (error) =>
return callback(error) if error?
@request.post {
url: "/project/#{project_id}/doc",
json: {
name: name,
parent_folder_id: parent_folder_id
}
}, (error, response, body) =>
callback(null, body._id)
addUserToProject: (project_id, user, privileges, callback = (error, user) ->) ->
if privileges == 'readAndWrite'
updateOp = {$addToSet: {collaberator_refs: user._id.toString()}}

View file

@ -23,8 +23,8 @@ describe "BetaProgramController", ->
optIn: sinon.stub()
optOut: sinon.stub()
},
"../User/UserLocator": @UserLocator = {
findById: sinon.stub()
"../User/UserGetter": @UserGetter = {
getUser: sinon.stub()
},
"settings-sharelatex": @settings = {
languages: {}
@ -119,7 +119,7 @@ describe "BetaProgramController", ->
describe "optInPage", ->
beforeEach ->
@UserLocator.findById.callsArgWith(1, null, @user)
@UserGetter.getUser.callsArgWith(1, null, @user)
it "should render the opt-in page", () ->
@BetaProgramController.optInPage @req, @res, @next
@ -128,10 +128,10 @@ describe "BetaProgramController", ->
args[0].should.equal 'beta_program/opt_in'
describe "when UserLocator.findById produces an error", ->
describe "when UserGetter.getUser produces an error", ->
beforeEach ->
@UserLocator.findById.callsArgWith(1, new Error('woops'))
@UserGetter.getUser.callsArgWith(1, new Error('woops'))
it "should not render the opt-in page", () ->
@BetaProgramController.optInPage @req, @res, @next

View file

@ -24,11 +24,14 @@ describe "CollaboratorsInviteController", ->
addCount: sinon.stub
@LimitationsManager = {}
@UserGetter =
getUserByMainEmail: sinon.stub()
getUser: sinon.stub()
@CollaboratorsInviteController = SandboxedModule.require modulePath, requires:
"../Project/ProjectGetter": @ProjectGetter = {}
'../Subscription/LimitationsManager' : @LimitationsManager
'../User/UserGetter': @UserGetter = {getUser: sinon.stub()}
'../User/UserGetter': @UserGetter
"./CollaboratorsHandler": @CollaboratorsHandler = {}
"./CollaboratorsInviteHandler": @CollaboratorsInviteHandler = {}
'logger-sharelatex': @logger = {err: sinon.stub(), error: sinon.stub(), log: sinon.stub()}
@ -713,7 +716,7 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@user = {_id: ObjectId().toString()}
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @user)
it 'should callback with `true`', (done) ->
@call (err, shouldAllow) =>
@ -725,7 +728,7 @@ describe "CollaboratorsInviteController", ->
beforeEach ->
@user = null
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @user)
it 'should callback with `false`', (done) ->
@call (err, shouldAllow) =>
@ -735,15 +738,15 @@ describe "CollaboratorsInviteController", ->
it 'should have called getUser', (done) ->
@call (err, shouldAllow) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @email}, {_id: 1}).should.equal true
@UserGetter.getUserByMainEmail.callCount.should.equal 1
@UserGetter.getUserByMainEmail.calledWith(@email, {_id: 1}).should.equal true
done()
describe 'when getUser produces an error', ->
beforeEach ->
@user = null
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, new Error('woops'))
it 'should callback with an error', (done) ->
@call (err, shouldAllow) =>

View file

@ -605,7 +605,7 @@ describe "CollaboratorsInviteHandler", ->
_id: ObjectId()
first_name: "jim"
@existingUser = {_id: ObjectId()}
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @existingUser)
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, @existingUser)
@fakeProject =
_id: @project_id
name: "some project"
@ -626,8 +626,8 @@ describe "CollaboratorsInviteHandler", ->
it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
@UserGetter.getUserByMainEmail.callCount.should.equal 1
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
done()
it 'should call getProject', (done) ->
@ -671,7 +671,7 @@ describe "CollaboratorsInviteHandler", ->
describe 'when the user does not exist', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, null)
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, null, null)
it 'should not produce an error', (done) ->
@call (err) =>
@ -680,8 +680,8 @@ describe "CollaboratorsInviteHandler", ->
it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
@UserGetter.getUserByMainEmail.callCount.should.equal 1
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
done()
it 'should not call getProject', (done) ->
@ -698,7 +698,7 @@ describe "CollaboratorsInviteHandler", ->
describe 'when the getUser produces an error', ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, new Error('woops'))
@UserGetter.getUserByMainEmail = sinon.stub().callsArgWith(2, new Error('woops'))
it 'should produce an error', (done) ->
@call (err) =>
@ -707,8 +707,8 @@ describe "CollaboratorsInviteHandler", ->
it 'should call getUser', (done) ->
@call (err) =>
@UserGetter.getUser.callCount.should.equal 1
@UserGetter.getUser.calledWith({email: @invite.email}).should.equal true
@UserGetter.getUserByMainEmail.callCount.should.equal 1
@UserGetter.getUserByMainEmail.calledWith(@invite.email).should.equal true
done()
it 'should not call getProject', (done) ->

View file

@ -16,7 +16,7 @@ describe "PasswordResetHandler", ->
getNewToken:sinon.stub()
getValueFromTokenAndExpire:sinon.stub()
@UserGetter =
getUser:sinon.stub()
getUserByMainEmail:sinon.stub()
@EmailHandler =
sendEmail:sinon.stub()
@AuthenticationManager =
@ -40,7 +40,7 @@ describe "PasswordResetHandler", ->
describe "generateAndEmailResetToken", ->
it "should check the user exists", (done)->
@UserGetter.getUser.callsArgWith(1)
@UserGetter.getUserByMainEmail.callsArgWith(1)
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
exists.should.equal false
@ -49,7 +49,7 @@ describe "PasswordResetHandler", ->
it "should send the email with the token", (done)->
@UserGetter.getUser.callsArgWith(1, null, @user)
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
@OneTimeTokenHandler.getNewToken.callsArgWith(1, null, @token)
@EmailHandler.sendEmail.callsArgWith(2)
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
@ -62,7 +62,7 @@ describe "PasswordResetHandler", ->
it "should return exists = false for a holdingAccount", (done) ->
@user.holdingAccount = true
@UserGetter.getUser.callsArgWith(1, null, @user)
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
exists.should.equal false

View file

@ -67,6 +67,7 @@ describe "ProjectController", ->
protectTokens: sinon.stub()
@CollaboratorsHandler =
userIsTokenMember: sinon.stub().callsArgWith(2, null, false)
@ProjectEntityHandler = {}
@Modules =
hooks:
fire: sinon.stub()
@ -98,6 +99,7 @@ describe "ProjectController", ->
"../TokenAccess/TokenAccessHandler": @TokenAccessHandler
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
"../../infrastructure/Modules": @Modules
"./ProjectEntityHandler": @ProjectEntityHandler
@projectName = "£12321jkj9ujkljds"
@req =
@ -520,7 +522,62 @@ describe "ProjectController", ->
@ProjectUpdateHandler.markAsOpened.calledWith(@project_id).should.equal true
done()
@ProjectController.loadEditor @req, @res
describe 'userProjectsJson', ->
beforeEach (done) ->
projects = [
{archived: true, id: 'a', name: 'A', accessLevel: 'a', somethingElse: 1}
{archived: false, id: 'b', name: 'B', accessLevel: 'b', somethingElse: 1}
{archived: false, id: 'c', name: 'C', accessLevel: 'c', somethingElse: 1}
{archived: false, id: 'd', name: 'D', accessLevel: 'd', somethingElse: 1}
]
@ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, [])
@ProjectController._buildProjectList = sinon.stub().returns(projects)
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
done()
it 'should produce a list of projects', (done) ->
@res.json = (data) =>
expect(data).to.deep.equal {
projects: [
{_id: 'b', name: 'B', accessLevel: 'b'},
{_id: 'c', name: 'C', accessLevel: 'c'},
{_id: 'd', name: 'D', accessLevel: 'd'}
]
}
done()
@ProjectController.userProjectsJson @req, @res, @next
describe 'projectEntitiesJson', ->
beforeEach () ->
@AuthenticationController.getLoggedInUserId = sinon.stub().returns 'abc'
@req.params = {Project_id: 'abcd'}
@project = { _id: 'abcd' }
@docs = [
{path: '/things/b.txt', doc: true},
{path: '/main.tex', doc: true}
]
@files = [
{path: '/things/a.txt'}
]
@ProjectGetter.getProject = sinon.stub().callsArgWith(1, null, @project)
@ProjectEntityHandler.getAllEntitiesFromProject = sinon.stub().callsArgWith(1, null, @docs, @files)
it 'should produce a list of entities', (done) ->
@res.json = (data) =>
expect(data).to.deep.equal {
project_id: 'abcd',
entities: [
{path: '/main.tex', type: 'doc'},
{path: '/things/a.txt', type: 'file'},
{path: '/things/b.txt', type: 'doc'}
]
}
expect(@ProjectGetter.getProject.callCount).to.equal 1
expect(@ProjectEntityHandler.getAllEntitiesFromProject.callCount).to.equal 1
done()
@ProjectController.projectEntitiesJson @req, @res, @next
describe '_isInPercentageRollout', ->
before ->
@ids = [

View file

@ -24,9 +24,6 @@ describe "SubscriptionGroupHandler", ->
getSubscriptionByMemberIdAndId: sinon.stub()
getSubscription: sinon.stub()
@UserCreator =
getUserOrCreateHoldingAccount: sinon.stub().callsArgWith(1, null, @user)
@SubscriptionUpdater =
addUserToGroup: sinon.stub().callsArgWith(2)
removeUserFromGroup: sinon.stub().callsArgWith(2)
@ -34,9 +31,9 @@ describe "SubscriptionGroupHandler", ->
@TeamInvitesHandler =
createManagerInvite: sinon.stub().callsArgWith(2)
@UserLocator =
findById: sinon.stub()
findByEmail: sinon.stub()
@UserGetter =
getUser: sinon.stub()
getUserByMainEmail: sinon.stub()
@LimitationsManager =
hasGroupMembersLimitReached: sinon.stub()
@ -61,7 +58,7 @@ describe "SubscriptionGroupHandler", ->
"./SubscriptionUpdater": @SubscriptionUpdater
"./TeamInvitesHandler": @TeamInvitesHandler
"./SubscriptionLocator": @SubscriptionLocator
"../User/UserLocator": @UserLocator
"../User/UserGetter": @UserGetter
"./LimitationsManager": @LimitationsManager
"../Security/OneTimeTokenHandler":@OneTimeTokenHandler
"../Email/EmailHandler":@EmailHandler
@ -76,11 +73,11 @@ describe "SubscriptionGroupHandler", ->
describe "addUserToGroup", ->
beforeEach ->
@LimitationsManager.hasGroupMembersLimitReached.callsArgWith(1, null, false, @subscription)
@UserLocator.findByEmail.callsArgWith(1, null, @user)
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
it "should find the user", (done)->
@Handler.addUserToGroup @adminUser_id, @newEmail, (err)=>
@UserLocator.findByEmail.calledWith(@newEmail).should.equal true
@UserGetter.getUserByMainEmail.calledWith(@newEmail).should.equal true
done()
it "should add the user to the group", (done)->
@ -105,9 +102,9 @@ describe "SubscriptionGroupHandler", ->
@NotificationsBuilder.groupPlan.calledWith(@user, {subscription_id:@subscription._id}).should.equal true
@readStub.called.should.equal true
done()
it "should add a team invite if no user is found", (done) ->
@UserLocator.findByEmail.callsArgWith(1, null, null)
it "should add an email invite if no user is found", (done) ->
@UserGetter.getUserByMainEmail.callsArgWith(1, null, null)
@Handler.addUserToGroup @adminUser_id, @newEmail, (err)=>
@TeamInvitesHandler.createManagerInvite.calledWith(@adminUser_id, @newEmail).should.equal true
done()
@ -124,26 +121,26 @@ describe "SubscriptionGroupHandler", ->
beforeEach ->
@subscription = {}
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription)
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
@UserGetter.getUser.callsArgWith(1, null, {_id:"31232"})
it "should locate the subscription", (done)->
@UserLocator.findById.callsArgWith(1, null, {_id:"31232"})
@UserGetter.getUser.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"})
@UserGetter.getUser.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
@UserLocator.findById.calledWith(@subscription.member_ids[1]).should.equal true
@UserLocator.findById.calledWith(@subscription.member_ids[2]).should.equal true
@UserGetter.getUser.calledWith(@subscription.member_ids[0]).should.equal true
@UserGetter.getUser.calledWith(@subscription.member_ids[1]).should.equal true
@UserGetter.getUser.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)
@UserGetter.getUser.callsArgWith(1)
@subscription.member_ids = ["1234", "342432", "312312"]
@Handler.getPopulatedListOfMembers @adminUser_id, (err, users)=>
assert.deepEqual users[0], {_id:@subscription.member_ids[0]}

View file

@ -0,0 +1,76 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = '../../../../app/js/Features/Templates/TemplatesController'
describe 'TemplatesController', ->
project_id = "213432"
beforeEach ->
@request = sinon.stub()
@request.returns {
pipe:->
on:->
}
@fs = {
unlink : sinon.stub()
createWriteStream : sinon.stub().returns(on:(_, cb)->cb())
}
@ProjectUploadManager = {createProjectFromZipArchive : sinon.stub().callsArgWith(3, null, {_id:project_id})}
@dumpFolder = "dump/path"
@ProjectOptionsHandler = {setCompiler:sinon.stub().callsArgWith(2)}
@uuid = "1234"
@ProjectDetailsHandler =
getProjectDescription:sinon.stub()
@Project =
update: sinon.stub().callsArgWith(3, null)
@controller = SandboxedModule.require modulePath, requires:
'../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager
'../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler
'../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()}
'./TemplatesPublisher':@TemplatesPublisher
"logger-sharelatex":
log:->
err:->
"settings-sharelatex":
path:
dumpFolder:@dumpFolder
siteUrl: @siteUrl = "http://localhost:3000"
apis:
v1:
url: @v1Url="http://overleaf.com"
user: "sharelatex"
pass: "password"
overleaf:
host: @v1Url
"uuid":v4:=>@uuid
"request": @request
"fs":@fs
"../../../../app/js/models/Project": {Project: @Project}
@zipUrl = "%2Ftemplates%2F52fb86a81ae1e566597a25f6%2Fv%2F4%2Fzip&templateName=Moderncv%20Banking&compiler=pdflatex"
@templateName = "project name here"
@user_id = "1234"
@req =
session:
user: _id:@user_id
templateData:
zipUrl: @zipUrl
templateName: @templateName
@redirect = {}
@AuthenticationController.getLoggedInUserId.returns(@user_id)
describe 'v1Templates', ->
it "should fetch zip from v1 based on template id", (done)->
@templateVersionId = 15
@req.body = {templateVersionId: @templateVersionId}
redirect = =>
@request.calledWith("#{@v1Url}/api/v1/sharelatex/templates/#{@templateVersionId}").should.equal true
done()
res = redirect:redirect
@controller.createProjectFromV1Template @req, res

View file

@ -30,8 +30,8 @@ describe "UserController", ->
@UserDeleter =
deleteUser: sinon.stub().callsArgWith(1)
@UserLocator =
findById: sinon.stub().callsArgWith(1, null, @user)
@UserGetter =
getUser: sinon.stub().callsArgWith(1, null, @user)
@User =
findById: sinon.stub().callsArgWith(1, null, @user)
@NewsLetterManager =
@ -63,7 +63,7 @@ describe "UserController", ->
@SudoModeHandler =
clearSudoMode: sinon.stub()
@UserController = SandboxedModule.require modulePath, requires:
"./UserLocator": @UserLocator
"./UserGetter": @UserGetter
"./UserDeleter": @UserDeleter
"./UserUpdater":@UserUpdater
"../../models/User": User:@User

View file

@ -15,34 +15,16 @@ describe "UserCreator", ->
constructor: ->
return self.user
@UserLocator =
findByEmail: sinon.stub()
@UserGetter =
getUserByMainEmail: sinon.stub()
@UserCreator = SandboxedModule.require modulePath, requires:
"../../models/User": User:@UserModel
"./UserLocator":@UserLocator
"./UserGetter":@UserGetter
"logger-sharelatex":{log:->}
'metrics-sharelatex': {timeAsyncMethod: ()->}
@email = "bob.oswald@gmail.com"
describe "getUserOrCreateHoldingAccount", ->
it "should immediately return the user if found", (done)->
@UserLocator.findByEmail.callsArgWith(1, null, @user)
@UserCreator.getUserOrCreateHoldingAccount @email, (err, returnedUser)=>
assert.deepEqual returnedUser, @user
done()
it "should create new holding account if the user is not found", (done)->
@UserLocator.findByEmail.callsArgWith(1)
@UserCreator.createNewUser = sinon.stub().callsArgWith(1, null, @user)
@UserCreator.getUserOrCreateHoldingAccount @email, (err, returnedUser)=>
@UserCreator.createNewUser.calledWith(email:@email, holdingAccount:true).should.equal true
assert.deepEqual returnedUser, @user
done()
describe "createNewUser", ->
it "should take the opts and put them in the model", (done)->

View file

@ -0,0 +1,58 @@
should = require('chai').should()
SandboxedModule = require('sandboxed-module')
assert = require('assert')
path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/User/UserGetter"
expect = require("chai").expect
describe "UserGetter", ->
beforeEach ->
@fakeUser = {_id:"12390i"}
@findOne = sinon.stub().callsArgWith(2, null, @fakeUser)
@Mongo =
db: users: findOne: @findOne
ObjectId: (id) -> return id
@UserGetter = SandboxedModule.require modulePath, requires:
"logger-sharelatex": log:->
"../../infrastructure/mongojs": @Mongo
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
describe "getUser", ->
it "should get user", (done)->
query = _id: 'foo'
projection = email: 1
@UserGetter.getUser query, projection, (error, user) =>
@findOne.called.should.equal true
@findOne.calledWith(query, projection).should.equal true
user.should.deep.equal @fakeUser
done()
it "should not allow email in query", (done)->
@UserGetter.getUser email: 'foo@bar.com', {}, (error, user) =>
error.should.exist
done()
describe "getUserbyMainEmail", ->
it "query user by main email", (done)->
email = 'hello@world.com'
projection = emails: 1
@UserGetter.getUserByMainEmail email, projection, (error, user) =>
@findOne.called.should.equal true
@findOne.calledWith(email: email, projection).should.equal true
done()
it "return user if found", (done)->
email = 'hello@world.com'
@UserGetter.getUserByMainEmail email, (error, user) =>
user.should.deep.equal @fakeUser
done()
it "trim email", (done)->
email = 'hello@world.com'
@UserGetter.getUserByMainEmail " #{email} ", (error, user) =>
@findOne.called.should.equal true
@findOne.calledWith(email: email).should.equal true
done()

View file

@ -1,39 +0,0 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/Features/User/UserLocator.js"
SandboxedModule = require('sandboxed-module')
describe "UserLocator", ->
beforeEach ->
@user = {_id:"12390i"}
@UserLocator = SandboxedModule.require modulePath, requires:
"../../infrastructure/mongojs": db: @db = { users: {} }
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
'logger-sharelatex' : { log: sinon.stub() }
@db.users =
findOne : sinon.stub().callsArgWith(1, null, @user)
@email = "bob.oswald@gmail.com"
describe "findByEmail", ->
it "should try and find a user with that email address", (done)->
@UserLocator.findByEmail @email, (err, user)=>
@db.users.findOne.calledWith(email:@email).should.equal true
done()
it "should trim white space", (done)->
@UserLocator.findByEmail "#{@email} ", (err, user)=>
@db.users.findOne.calledWith(email:@email).should.equal true
done()
it "should return the user if found", (done)->
@UserLocator.findByEmail @email, (err, user)=>
user.should.deep.equal @user
done()

View file

@ -16,10 +16,7 @@ describe "UserPagesController", ->
features:{}
email: "joe@example.com"
@UserLocator =
findById: sinon.stub().callsArgWith(1, null, @user)
@UserGetter =
getUser: sinon.stub().callsArgWith(2, null, @user)
@UserGetter = getUser: sinon.stub()
@UserSessionsManager =
getAllUserSessions: sinon.stub()
@dropboxStatus = {}
@ -37,7 +34,6 @@ describe "UserPagesController", ->
"logger-sharelatex":
log:->
err:->
"./UserLocator": @UserLocator
"./UserGetter": @UserGetter
"./UserSessionsManager": @UserSessionsManager
"../Errors/ErrorController": @ErrorController
@ -136,6 +132,8 @@ describe "UserPagesController", ->
@UserPagesController.sessionsPage @req, @res, @next
describe "settingsPage", ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(1, null, @user)
it "should render user/settings", (done)->
@res.render = (page)->
@ -185,6 +183,7 @@ describe "UserPagesController", ->
describe "activateAccountPage", ->
beforeEach ->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
@req.query.user_id = @user_id
@req.query.token = @token = "mock-token-123"

View file

@ -12,8 +12,9 @@ describe "UserRegistrationHandler", ->
@user =
_id: @user_id = "31j2lk21kjl"
@User =
findOne:sinon.stub()
update: sinon.stub().callsArgWith(2)
@UserGetter =
getUserByMainEmail: sinon.stub()
@UserCreator =
createNewUser:sinon.stub().callsArgWith(1, null, @user)
@AuthenticationManager =
@ -26,6 +27,7 @@ describe "UserRegistrationHandler", ->
getNewToken: sinon.stub()
@handler = SandboxedModule.require modulePath, requires:
"../../models/User": {User:@User}
"./UserGetter": @UserGetter
"./UserCreator": @UserCreator
"../Authentication/AuthenticationManager":@AuthenticationManager
"../Newsletter/NewsletterManager":@NewsLetterManager
@ -70,7 +72,7 @@ describe "UserRegistrationHandler", ->
beforeEach ->
@user.holdingAccount = true
@handler._registrationRequestIsValid = sinon.stub().returns true
@User.findOne.callsArgWith(1, null, @user)
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
it "should not create a new user if there is a holding account there", (done)->
@handler.registerNewUser @passingRequest, (err)=>
@ -94,7 +96,7 @@ describe "UserRegistrationHandler", ->
done()
it "should return email registered in the error if there is a non holdingAccount there", (done)->
@User.findOne.callsArgWith(1, null, @user = {holdingAccount:false})
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user = {holdingAccount:false})
@handler.registerNewUser @passingRequest, (err, user)=>
err.should.deep.equal new Error("EmailAlreadyRegistered")
user.should.deep.equal @user
@ -103,7 +105,7 @@ describe "UserRegistrationHandler", ->
describe "validRequest", ->
beforeEach ->
@handler._registrationRequestIsValid = sinon.stub().returns true
@User.findOne.callsArgWith 1
@UserGetter.getUserByMainEmail.callsArgWith 1
it "should create a new user", (done)->
@handler.registerNewUser @passingRequest, (err)=>

View file

@ -14,12 +14,12 @@ describe "UserUpdater", ->
@mongojs =
db:{}
ObjectId:(id)-> return id
@UserLocator =
findByEmail:sinon.stub()
@UserGetter =
getUserByMainEmail: sinon.stub()
@UserUpdater = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex": log:->
"./UserLocator":@UserLocator
"./UserGetter": @UserGetter
"../../infrastructure/mongojs":@mongojs
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
@ -34,7 +34,7 @@ describe "UserUpdater", ->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2)
it "should check if the new email already has an account", (done)->
@UserLocator.findByEmail.callsArgWith(1, null, @stubbedUser)
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @stubbedUser)
@UserUpdater.changeEmailAddress @user_id, @stubbedUser.email, (err)=>
@UserUpdater.updateUser.called.should.equal false
should.exist(err)
@ -42,7 +42,7 @@ describe "UserUpdater", ->
it "should set the users password", (done)->
@UserLocator.findByEmail.callsArgWith(1, null)
@UserGetter.getUserByMainEmail.callsArgWith(1, null)
@UserUpdater.changeEmailAddress @user_id, @newEmail, (err)=>
@UserUpdater.updateUser.calledWith(@user_id, $set: { "email": @newEmail}).should.equal true
done()