diff --git a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee index 4e96ce113c..1e0577cfc1 100644 --- a/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee +++ b/services/web/app/coffee/Features/BetaProgram/BetaProgramController.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee index 898abe52bb..f74a144bac 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee @@ -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) diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee index ecca8ab86f..b511f56e53 100644 --- a/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee +++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee @@ -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) diff --git a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee index aaf4172cf4..1a5e13e86e 100644 --- a/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/LinkedFilesController.coffee @@ -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 -} \ No newline at end of file + 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 + } diff --git a/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee new file mode 100644 index 0000000000..5ea4554426 --- /dev/null +++ b/services/web/app/coffee/Features/LinkedFiles/ProjectFileAgent.coffee @@ -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() diff --git a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee index ad96aa628f..7a15fe52d3 100644 --- a/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee +++ b/services/web/app/coffee/Features/LinkedFiles/UrlAgent.coffee @@ -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)}" -} \ No newline at end of file +} diff --git a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee b/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee index 4e67e9f1f4..3947b63004 100644 --- a/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee +++ b/services/web/app/coffee/Features/PasswordReset/PasswordResetHandler.coffee @@ -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" diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 4ca886fed0..8ea2dc3189 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee index 27ead91841..22034200f5 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityUpdateHandler.coffee @@ -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)-> diff --git a/services/web/app/coffee/Features/References/ReferencesHandler.coffee b/services/web/app/coffee/Features/References/ReferencesHandler.coffee index 8728896631..959833351f 100644 --- a/services/web/app/coffee/Features/References/ReferencesHandler.coffee +++ b/services/web/app/coffee/Features/References/ReferencesHandler.coffee @@ -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 = diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee index b8d1b32aa0..619ee49351 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionGroupHandler.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee new file mode 100644 index 0000000000..fce6c9502c --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesController.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddlewear b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear new file mode 100644 index 0000000000..300721c889 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear @@ -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() diff --git a/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee new file mode 100644 index 0000000000..8baa0ca605 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesMiddlewear.coffee @@ -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() diff --git a/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee b/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee new file mode 100644 index 0000000000..3061789591 --- /dev/null +++ b/services/web/app/coffee/Features/Templates/TemplatesRouter.coffee @@ -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 diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee index be1874f0cd..744b77b4de 100644 --- a/services/web/app/coffee/Features/User/UserController.coffee +++ b/services/web/app/coffee/Features/User/UserController.coffee @@ -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() diff --git a/services/web/app/coffee/Features/User/UserCreator.coffee b/services/web/app/coffee/Features/User/UserCreator.coffee index d08b953559..0a0cc8641e 100644 --- a/services/web/app/coffee/Features/User/UserCreator.coffee +++ b/services/web/app/coffee/Features/User/UserCreator.coffee @@ -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() diff --git a/services/web/app/coffee/Features/User/UserGetter.coffee b/services/web/app/coffee/Features/User/UserGetter.coffee index 306fbc7a10..201981625e 100644 --- a/services/web/app/coffee/Features/User/UserGetter.coffee +++ b/services/web/app/coffee/Features/User/UserGetter.coffee @@ -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) -> diff --git a/services/web/app/coffee/Features/User/UserLocator.coffee b/services/web/app/coffee/Features/User/UserLocator.coffee deleted file mode 100644 index 9be32c76b0..0000000000 --- a/services/web/app/coffee/Features/User/UserLocator.coffee +++ /dev/null @@ -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 diff --git a/services/web/app/coffee/Features/User/UserPagesController.coffee b/services/web/app/coffee/Features/User/UserPagesController.coffee index 25825c35e6..5e6ea7d62b 100644 --- a/services/web/app/coffee/Features/User/UserPagesController.coffee +++ b/services/web/app/coffee/Features/User/UserPagesController.coffee @@ -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' diff --git a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee index f5db2e54a1..fab438ffa6 100644 --- a/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee +++ b/services/web/app/coffee/Features/User/UserRegistrationHandler.coffee @@ -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 diff --git a/services/web/app/coffee/Features/User/UserUpdater.coffee b/services/web/app/coffee/Features/User/UserUpdater.coffee index 530d81063d..174d37bc38 100644 --- a/services/web/app/coffee/Features/User/UserUpdater.coffee +++ b/services/web/app/coffee/Features/User/UserUpdater.coffee @@ -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(), { diff --git a/services/web/app/coffee/infrastructure/FileWriter.coffee b/services/web/app/coffee/infrastructure/FileWriter.coffee index dedeed9bad..27b1f16921 100644 --- a/services/web/app/coffee/infrastructure/FileWriter.coffee +++ b/services/web/app/coffee/infrastructure/FileWriter.coffee @@ -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) \ No newline at end of file + callback(err) diff --git a/services/web/app/coffee/infrastructure/Modules.coffee b/services/web/app/coffee/infrastructure/Modules.coffee index 769182ad94..e1b2e11520 100644 --- a/services/web/app/coffee/infrastructure/Modules.coffee +++ b/services/web/app/coffee/infrastructure/Modules.coffee @@ -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 [] diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index e6b2692f7c..b7481725a2 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -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 diff --git a/services/web/app/views/contact-us-modal.pug b/services/web/app/views/contact-us-modal.pug deleted file mode 100644 index eacfd10533..0000000000 --- a/services/web/app/views/contact-us-modal.pug +++ /dev/null @@ -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: "" + translate("knowledge_base") + "" })} - ul.contact-suggestion-list - li(ng-repeat="suggestion in suggestions") - a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank") - span(ng-bind-html="suggestion.name") - i.fa.fa-angle-right - label.desc(ng-show="'"+getUserEmail()+"'.length < 1") - | #{translate("email")} - .form-group(ng-show="'"+getUserEmail()+"'.length < 1") - 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")} diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 4dfdbc3ae7..687f5e1880 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -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 diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug index e5690c1874..35d75c9fb3 100644 --- a/services/web/app/views/project/editor/binary-file.pug +++ b/services/web/app/views/project/editor/binary-file.pug @@ -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}} diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index 3cac3a9490..c24d967da7 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -33,11 +33,11 @@ div.full-size( i.fa.fa-arrow-left |   #{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 diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug index 55fc660abc..d16258d1c9 100644 --- a/services/web/app/views/project/editor/file-tree.pug +++ b/services/web/app/views/project/editor/file-tree.pug @@ -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") + |   + 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") + |   + 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 + |   + 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 diff --git a/services/web/app/views/project/editor/history/diffPanelV1.pug b/services/web/app/views/project/editor/history/diffPanelV1.pug index 1720f48b59..30588adf46 100644 --- a/services/web/app/views/project/editor/history/diffPanelV1.pug +++ b/services/web/app/views/project/editor/history/diffPanelV1.pug @@ -13,7 +13,7 @@ }" ) | in {{history.diff.pathname}} - .toolbar-right + .toolbar-right(ng-if="permissions.write") a.btn.btn-danger.btn-sm( href, ng-click="openRestoreDiffModal()" diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug index ed6ef85b44..d85a45723b 100644 --- a/services/web/app/views/project/editor/left-menu.pug +++ b/services/web/app/views/project/editor/left-menu.pug @@ -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") diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug new file mode 100644 index 0000000000..6dc27a4241 --- /dev/null +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -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) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index bfc53a8360..bc37683489 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -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 \ No newline at end of file diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug index df3c2bf681..cfea4aa6c6 100644 --- a/services/web/app/views/project/list/project-list.pug +++ b/services/web/app/views/project/list/project-list.pug @@ -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" diff --git a/services/web/app/views/project/list/v1-item.pug b/services/web/app/views/project/list/v1-item.pug index b4a3ccb99d..5a8e37bca0 100644 --- a/services/web/app/views/project/list/v1-item.pug +++ b/services/web/app/views/project/list/v1-item.pug @@ -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}} \ No newline at end of file diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee index 0892804778..7f4024c368 100644 --- a/services/web/config/settings.defaults.coffee +++ b/services/web/config/settings.defaults.coffee @@ -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: diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml index a062c0df4e..9596fe5126 100644 --- a/services/web/docker-compose.yml +++ b/services/web/docker-compose.yml @@ -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 diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 7c8602eb76..6c5b1d920e 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -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 = () ->) -> diff --git a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee index c14e097842..bba455c447 100644 --- a/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee +++ b/services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee @@ -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) diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index e3cabf8e98..7246e09b83 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -14,7 +14,7 @@ define [ opening: true trackChanges: false wantTrackChanges: false - richText: false + showRichText: false } @$scope.$on "entity:selected", (event, entity) => diff --git a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee index 3d4077b2dd..010e00476f 100644 --- a/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee +++ b/services/web/public/coffee/ide/file-tree/controllers/FileTreeController.coffee @@ -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) -> diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee index c2ced4cf59..279c230afb 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DiffController.coffee @@ -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) - \ No newline at end of file + 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) diff --git a/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee new file mode 100644 index 0000000000..ae7db45905 --- /dev/null +++ b/services/web/public/coffee/ide/test-controls/controllers/TestControlsController.coffee @@ -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 diff --git a/services/web/public/coffee/ide/test-controls/index.coffee b/services/web/public/coffee/ide/test-controls/index.coffee new file mode 100644 index 0000000000..d60d9e1a01 --- /dev/null +++ b/services/web/public/coffee/ide/test-controls/index.coffee @@ -0,0 +1,3 @@ +define [ + "ide/test-controls/controllers/TestControlsController" +], () -> diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 2d1dee6d34..6d3e441c49 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -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: "
browser: #{platform?.name} #{platform?.version}
-
os: #{platform?.os?.family} #{platform?.os?.version}
" - - 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 = {} diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index 36520d2cc7..c2b251bd65 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -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]) diff --git a/services/web/public/coffee/services/algolia-search.coffee b/services/web/public/coffee/services/algolia-search.coffee index d62bc6389d..9ae5eae077 100644 --- a/services/web/public/coffee/services/algolia-search.coffee +++ b/services/web/public/coffee/services/algolia-search.coffee @@ -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 \ No newline at end of file diff --git a/services/web/public/coffee/services/wait-for.coffee b/services/web/public/coffee/services/wait-for.coffee new file mode 100644 index 0000000000..409142354c --- /dev/null +++ b/services/web/public/coffee/services/wait-for.coffee @@ -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 diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 982086a262..569035ad72 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -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; diff --git a/services/web/public/stylesheets/components/forms.less b/services/web/public/stylesheets/components/forms.less index 8eb92411cd..00653b105b 100755 --- a/services/web/public/stylesheets/components/forms.less +++ b/services/web/public/stylesheets/components/forms.less @@ -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; + } } diff --git a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee index 9ca7ecae42..f3e8694e2a 100644 --- a/services/web/test/acceptance/coffee/LinkedFilesTests.coffee +++ b/services/web/test/acceptance/coffee/LinkedFilesTests.coffee @@ -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) -> diff --git a/services/web/test/acceptance/coffee/helpers/User.coffee b/services/web/test/acceptance/coffee/helpers/User.coffee index a02ab5c42c..61c2e84a8c 100644 --- a/services/web/test/acceptance/coffee/helpers/User.coffee +++ b/services/web/test/acceptance/coffee/helpers/User.coffee @@ -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()}} diff --git a/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee b/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee index ab1f1b0567..713179b056 100644 --- a/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee +++ b/services/web/test/unit/coffee/BetaProgram/BetaProgramControllerTests.coffee @@ -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 diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee index cdfff78249..4b57ff3697 100644 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee +++ b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee @@ -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) => diff --git a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee index 177c42d4ba..58b373f61f 100644 --- a/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee +++ b/services/web/test/unit/coffee/Collaborators/CollaboratorsInviteHandlerTests.coffee @@ -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) -> diff --git a/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee b/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee index b29839246a..261f5582dd 100644 --- a/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee +++ b/services/web/test/unit/coffee/PasswordReset/PasswordResetHandlerTests.coffee @@ -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 diff --git a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee index ec7246754c..1b66ba9bf9 100644 --- a/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectControllerTests.coffee @@ -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 = [ diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee index 878751df2e..4a2e01fb05 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionGroupHandlerTests.coffee @@ -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]} diff --git a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee new file mode 100644 index 0000000000..5cf52eca39 --- /dev/null +++ b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee @@ -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 diff --git a/services/web/test/unit/coffee/User/UserControllerTests.coffee b/services/web/test/unit/coffee/User/UserControllerTests.coffee index a9ab47e6cd..17ac009789 100644 --- a/services/web/test/unit/coffee/User/UserControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserControllerTests.coffee @@ -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 diff --git a/services/web/test/unit/coffee/User/UserCreatorTests.coffee b/services/web/test/unit/coffee/User/UserCreatorTests.coffee index 8470e5621f..cc2b1ec150 100644 --- a/services/web/test/unit/coffee/User/UserCreatorTests.coffee +++ b/services/web/test/unit/coffee/User/UserCreatorTests.coffee @@ -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)-> diff --git a/services/web/test/unit/coffee/User/UserGetterTests.coffee b/services/web/test/unit/coffee/User/UserGetterTests.coffee new file mode 100644 index 0000000000..2c32bd8250 --- /dev/null +++ b/services/web/test/unit/coffee/User/UserGetterTests.coffee @@ -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() diff --git a/services/web/test/unit/coffee/User/UserLocatorTests.coffee b/services/web/test/unit/coffee/User/UserLocatorTests.coffee deleted file mode 100644 index dc3fc84dfa..0000000000 --- a/services/web/test/unit/coffee/User/UserLocatorTests.coffee +++ /dev/null @@ -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() - - - diff --git a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee b/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee index 529f5b1be6..a0f155846f 100644 --- a/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee +++ b/services/web/test/unit/coffee/User/UserPagesControllerTests.coffee @@ -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" diff --git a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee index d0b96da2de..9411059022 100644 --- a/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee +++ b/services/web/test/unit/coffee/User/UserRegistrationHandlerTests.coffee @@ -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)=> diff --git a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee index a6239e2e65..202a4f3f1a 100644 --- a/services/web/test/unit/coffee/User/UserUpdaterTests.coffee +++ b/services/web/test/unit/coffee/User/UserUpdaterTests.coffee @@ -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()