diff --git a/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee b/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee index 2f4e2b7932..e73dc06eb2 100644 --- a/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee +++ b/services/web/app/coffee/Features/Downloads/ProjectDownloadsController.coffee @@ -32,7 +32,7 @@ module.exports = ProjectDownloadsController = return next(error) if error? res.setContentDisposition( 'attachment', - {filename: "ShareLaTeX Projects (#{project_ids.length} items).zip"} + {filename: "Overleaf Projects (#{project_ids.length} items).zip"} ) res.contentType('application/zip') stream.pipe(res) diff --git a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee index 9767d148b9..4f3f9b6b1e 100644 --- a/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectDetailsHandler.coffee @@ -67,6 +67,48 @@ module.exports = ProjectDetailsHandler = else return callback() + _addSuffixToProjectName: (name, suffix = '') -> + # append the suffix and truncate the project title if needed + truncatedLength = ProjectDetailsHandler.MAX_PROJECT_NAME_LENGTH - suffix.length + return name.substr(0, truncatedLength) + suffix + + # FIXME: we should put a lock around this to make it completely safe, but we would need to do that at + # the point of project creation, rather than just checking the name at the start of the import. + # If we later move this check into ProjectCreationHandler we can ensure all new projects are created + # with a unique name. But that requires thinking through how we would handle incoming projects from + # dropbox for example. + ensureProjectNameIsUnique: (user_id, name, suffixes = [], callback = (error, name, changed)->) -> + ProjectGetter.findAllUsersProjects user_id, {name: 1}, (error, allUsersProjectNames) -> + return callback(error) if error? + # allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]} + # collect all of the names and flatten them into a single array + projectNameList = _.flatten(_.values(allUsersProjectNames)) + # create a set of all project names + allProjectNames = new Set() + for projectName in projectNameList + allProjectNames.add(projectName) + isUnique = (x) -> !allProjectNames.has(x) + # check if the supplied name is already unique + if isUnique(name) + return callback(null, name, false) + # the name already exists, try adding the user-supplied suffixes to generate a unique name + for suffix in suffixes + candidateName = ProjectDetailsHandler._addSuffixToProjectName(name, suffix) + if isUnique(candidateName) + return callback(null, candidateName, true) + # we couldn't make the name unique, something is wrong + return callback new Errors.InvalidNameError("Project name could not be made unique") + + fixProjectName: (name) -> + if name == "" + name = "Untitled" + if name.indexOf('/') > -1 + # v2 does not allow / in a project name + name = name.replace(/\//g, '-') + if name.length > @MAX_PROJECT_NAME_LENGTH + name = name.substr(0, @MAX_PROJECT_NAME_LENGTH) + return name + setPublicAccessLevel : (project_id, newAccessLevel, callback = ->)-> logger.log project_id: project_id, level: newAccessLevel, "set public access level" # DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 5994521b8e..2160fbc419 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -65,6 +65,12 @@ module.exports = ProjectEntityHandler = self = files.push({path: path.join(folderPath, file.name), file:file}) callback null, docs, files + getAllDocPathsFromProjectById: (project_id, callback) -> + ProjectGetter.getProjectWithoutDocLines project_id, (err, project) -> + return callback(err) if err? + return callback(Errors.NotFoundError("no project")) if !project? + self.getAllDocPathsFromProject project, callback + getAllDocPathsFromProject: (project, callback) -> logger.log project:project, "getting all docs for project" self._getAllFoldersFromProject project, (err, folders = {}) -> diff --git a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee index 4530fb67d3..973d2b2ed6 100644 --- a/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee +++ b/services/web/app/coffee/Features/Project/ProjectRootDocManager.coffee @@ -31,3 +31,27 @@ module.exports = ProjectRootDocManager = ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback else callback() + + setRootDocFromName: (project_id, rootDocName, callback = (error) ->) -> + ProjectEntityHandler.getAllDocPathsFromProjectById project_id, (error, docPaths) -> + return callback(error) if error? + # strip off leading and trailing quotes from rootDocName + rootDocName = rootDocName.replace(/^\'|\'$/g,"") + # prepend a slash for the root folder if not present + rootDocName = "/#{rootDocName}" if rootDocName[0] isnt '/' + # find the root doc from the filename + root_doc_id = null + for doc_id, path of docPaths + # docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" + if path == rootDocName + root_doc_id = doc_id + # try a basename match if there was no match + if !root_doc_id + for doc_id, path of docPaths + if Path.basename(path) == Path.basename(rootDocName) + root_doc_id = doc_id + # set the root doc id if we found a match + if root_doc_id? + ProjectEntityUpdateHandler.setRootDoc project_id, root_doc_id, callback + else + callback() \ No newline at end of file diff --git a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee index 7bbfd78780..b089f943ce 100644 --- a/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/TeamInvitesHandler.coffee @@ -50,7 +50,9 @@ module.exports = TeamInvitesHandler = email = EmailHelper.parseEmail(user.email) return callback(new Error('invalid email')) if !email? logger.log {licence, email: email}, "Creating domain team invite" - inviterName = licence.name.replace(/\s+licence$/i, licence.name) + # If name == 'Uni of X License', make the email read only + # 'Uni of X has invited you...' + inviterName = licence.name.replace(/\s+(site\s+)?licence$/i, '') SubscriptionLocator.getSubscription licence.subscription_id, (error, subscription) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Templates/TemplatesController.coffee b/services/web/app/coffee/Features/Templates/TemplatesController.coffee index fce6c9502c..742ee4a7d1 100644 --- a/services/web/app/coffee/Features/Templates/TemplatesController.coffee +++ b/services/web/app/coffee/Features/Templates/TemplatesController.coffee @@ -2,6 +2,8 @@ path = require('path') Project = require('../../../js/models/Project').Project ProjectUploadManager = require('../../../js/Features/Uploads/ProjectUploadManager') ProjectOptionsHandler = require('../../../js/Features/Project/ProjectOptionsHandler') +ProjectRootDocManager = require('../../../js/Features/Project/ProjectRootDocManager') +ProjectDetailsHandler = require('../../../js/Features/Project/ProjectDetailsHandler') AuthenticationController = require('../../../js/Features/Authentication/AuthenticationController') settings = require('settings-sharelatex') fs = require('fs') @@ -24,6 +26,7 @@ module.exports = TemplatesController = data.templateId = templateId data.name = req.query.templateName data.compiler = req.query.latexEngine + data.mainFile = req.query.mainFile res.render path.resolve(__dirname, "../../../views/project/editor/new_from_template"), data createProjectFromV1Template: (req, res)-> @@ -43,14 +46,18 @@ module.exports = TemplatesController = currentUserId: currentUserId, compiler: req.body.compiler docId: req.body.docId + mainFile: req.body.mainFile templateId: req.body.templateId templateVersionId: req.body.templateVersionId + image: 'wl_texlive:2018.1' }, req, res ) createFromZip: (zipReq, options, req, res)-> + # remove any invalid characters from template name + projectName = ProjectDetailsHandler.fixProjectName(options.templateName) dumpPath = "#{settings.path.dumpFolder}/#{uuid.v4()}" writeStream = fs.createWriteStream(dumpPath) @@ -58,23 +65,37 @@ module.exports = TemplatesController = 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)-> + ProjectUploadManager.createProjectFromZipArchive options.currentUserId, projectName, 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}" + setImage project._id, options.image, -> + setMainFile project._id, options.mainFile, -> + 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() + +setImage = (project_id, imageName, callback)-> + if imageName? + ProjectOptionsHandler.setImageName project_id, imageName, callback + else + callback() + +setMainFile = (project_id, mainFile, callback) -> + if mainFile? + ProjectRootDocManager.setRootDocFromName project_id, mainFile, callback + else + callback() diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee index 08aa4663f1..d58b28cb30 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee @@ -1,6 +1,7 @@ ProjectController = require "../Project/ProjectController" AuthenticationController = require '../Authentication/AuthenticationController' TokenAccessHandler = require './TokenAccessHandler' +V1Api = require '../V1/V1Api' Errors = require '../Errors/Errors' logger = require 'logger-sharelatex' settings = require 'settings-sharelatex' @@ -12,16 +13,11 @@ module.exports = TokenAccessController = return ProjectController.loadEditor(req, res, next) _tryHigherAccess: (token, userId, req, res, next) -> - TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project, projectExists) -> + TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project) -> if err? logger.err {err, token, userId}, "[TokenAccess] error finding project with higher access" return next(err) - if !projectExists and settings.overleaf - logger.log {token, userId}, - "[TokenAccess] no project found for this token" - # Project does not exist, but may be unimported - try it on v1 - return res.redirect(settings.overleaf.host + req.url) if !project? logger.log {token, userId}, "[TokenAccess] no project with higher access found for this user and token" @@ -34,12 +30,19 @@ module.exports = TokenAccessController = userId = AuthenticationController.getLoggedInUserId(req) token = req.params['read_and_write_token'] logger.log {userId, token}, "[TokenAccess] requesting read-and-write token access" - TokenAccessHandler.findProjectWithReadAndWriteToken token, (err, project) -> + TokenAccessHandler.findProjectWithReadAndWriteToken token, (err, project, projectExists) -> if err? logger.err {err, token, userId}, "[TokenAccess] error getting project by readAndWrite token" return next(err) - if !project? + if !projectExists and settings.overleaf + logger.log {token, userId}, + "[TokenAccess] no project found for this token" + TokenAccessHandler.checkV1ProjectExported token, (err, exported) -> + return next err if err? + return next(new Errors.NotFoundError()) if exported + return res.redirect(302, "/sign_in_to_v1?return_to=/#{token}") + else if !project? logger.log {token, userId}, "[TokenAccess] no token-based project found for readAndWrite token" if !userId? @@ -77,12 +80,19 @@ module.exports = TokenAccessController = userId = AuthenticationController.getLoggedInUserId(req) token = req.params['read_only_token'] logger.log {userId, token}, "[TokenAccess] requesting read-only token access" - TokenAccessHandler.findProjectWithReadOnlyToken token, (err, project) -> + TokenAccessHandler.findProjectWithReadOnlyToken token, (err, project, projectExists) -> if err? logger.err {err, token, userId}, "[TokenAccess] error getting project by readOnly token" return next(err) - if !project? + if !projectExists and settings.overleaf + logger.log {token, userId}, + "[TokenAccess] no project found for this token" + TokenAccessHandler.checkV1ProjectExported token, (err, exported) -> + return next err if err? + return next(new Errors.NotFoundError()) if exported + return res.redirect(302, "/sign_in_to_v1?return_to=/read/#{token}") + else if !project? logger.log {token, userId}, "[TokenAccess] no project found for readOnly token" if !userId? @@ -91,23 +101,26 @@ module.exports = TokenAccessController = return next(new Errors.NotFoundError()) TokenAccessController._tryHigherAccess(token, userId, req, res, next) else - if !userId? - logger.log {userId, projectId: project._id}, - "[TokenAccess] adding anonymous user to project with readOnly token" - TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) - req._anonymousAccessToken = token - return TokenAccessController._loadEditor(project._id, req, res, next) - else - if project.owner_ref.toString() == userId + TokenAccessHandler.checkV1Access token, (err, allow_access, redirect_path) -> + return next err if err? + return res.redirect redirect_path unless allow_access + if !userId? logger.log {userId, projectId: project._id}, - "[TokenAccess] user is already project owner" - return TokenAccessController._loadEditor(project._id, req, res, next) - logger.log {userId, projectId: project._id}, - "[TokenAccess] adding user to project with readOnly token" - TokenAccessHandler.addReadOnlyUserToProject userId, project._id, (err) -> - if err? - logger.err {err, token, userId, projectId: project._id}, - "[TokenAccess] error adding user to project with readAndWrite token" - return next(err) + "[TokenAccess] adding anonymous user to project with readOnly token" + TokenAccessHandler.grantSessionTokenAccess(req, project._id, token) + req._anonymousAccessToken = token return TokenAccessController._loadEditor(project._id, req, res, next) + else + if project.owner_ref.toString() == userId + logger.log {userId, projectId: project._id}, + "[TokenAccess] user is already project owner" + return TokenAccessController._loadEditor(project._id, req, res, next) + logger.log {userId, projectId: project._id}, + "[TokenAccess] adding user to project with readOnly token" + TokenAccessHandler.addReadOnlyUserToProject userId, project._id, (err) -> + if err? + logger.err {err, token, userId, projectId: project._id}, + "[TokenAccess] error adding user to project with readAndWrite token" + return next(err) + return TokenAccessController._loadEditor(project._id, req, res, next) diff --git a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee b/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee index ed7f51f0d7..100786f776 100644 --- a/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee +++ b/services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee @@ -4,25 +4,38 @@ PublicAccessLevels = require '../Authorization/PublicAccessLevels' PrivilegeLevels = require '../Authorization/PrivilegeLevels' ObjectId = require("mongojs").ObjectId Settings = require('settings-sharelatex') +V1Api = require "../V1/V1Api" module.exports = TokenAccessHandler = ANONYMOUS_READ_AND_WRITE_ENABLED: Settings.allowAnonymousReadAndWriteSharing == true - findProjectWithReadOnlyToken: (token, callback=(err, project)->) -> + findProjectWithReadOnlyToken: (token, callback=(err, project, projectExists)->) -> Project.findOne { - 'tokens.readOnly': token, - 'publicAccesLevel': PublicAccessLevels.TOKEN_BASED - }, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback + 'tokens.readOnly': token + }, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, (err, project) -> + if err? + return callback(err) + if !project? + return callback(null, null, false) # Project doesn't exist, so we handle differently + if project.publicAccesLevel != PublicAccessLevels.TOKEN_BASED + return callback(null, null, true) # Project does exist, but it isn't token based + return callback(null, project, true) - findProjectWithReadAndWriteToken: (token, callback=(err, project)->) -> + findProjectWithReadAndWriteToken: (token, callback=(err, project, projectExists)->) -> Project.findOne { - 'tokens.readAndWrite': token, - 'publicAccesLevel': PublicAccessLevels.TOKEN_BASED - }, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback + 'tokens.readAndWrite': token + }, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, (err, project) -> + if err? + return callback(err) + if !project? + return callback(null, null, false) # Project doesn't exist, so we handle differently + if project.publicAccesLevel != PublicAccessLevels.TOKEN_BASED + return callback(null, null, true) # Project does exist, but it isn't token based + return callback(null, project, true) - findProjectWithHigherAccess: (token, userId, callback=(err, project, projectExists)->) -> + findProjectWithHigherAccess: (token, userId, callback=(err, project)->) -> Project.findOne { $or: [ {'tokens.readAndWrite': token}, @@ -32,15 +45,14 @@ module.exports = TokenAccessHandler = if err? return callback(err) if !project? - return callback(null, null, false) # Project doesn't exist, so we handle differently + return callback(null, null) projectId = project._id CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, (err, isMember) -> if err? return callback(err) callback( null, - if isMember == true then project else null, - true # Project does exist, but user doesn't have access + if isMember == true then project else null ) addReadOnlyUserToProject: (userId, projectId, callback=(err)->) -> @@ -97,3 +109,16 @@ module.exports = TokenAccessHandler = project.tokens.readAndWrite = '' if privilegeLevel != PrivilegeLevels.READ_ONLY project.tokens.readOnly = '' + + checkV1Access: (token, callback=(err, allow, redirect)->) -> + return callback(null, true) unless Settings.apis?.v1? + V1Api.request { url: "/api/v1/sharelatex/docs/#{token}/is_published" }, (err, response, body) -> + return callback err if err? + callback null, false, body.published_path if body.allow == false + callback null, true + + checkV1ProjectExported: (token, callback = (err, exists) ->) -> + return callback(null, false) unless Settings.apis?.v1? + V1Api.request { url: "/api/v1/sharelatex/docs/#{token}/exported_to_v2" }, (err, response, body) -> + return callback err if err? + callback null, body.exported diff --git a/services/web/app/coffee/Features/V1/V1Api.coffee b/services/web/app/coffee/Features/V1/V1Api.coffee new file mode 100644 index 0000000000..6e781ef147 --- /dev/null +++ b/services/web/app/coffee/Features/V1/V1Api.coffee @@ -0,0 +1,26 @@ +request = require 'request' +settings = require 'settings-sharelatex' + +# TODO: check what happens when these settings aren't defined +DEFAULT_V1_PARAMS = { + baseUrl: settings?.apis?.v1?.url + auth: + user: settings?.apis?.v1?.user + pass: settings?.apis?.v1?.pass + json: true, + timeout: 30 * 1000 +} + +request = request.defaults(DEFAULT_V1_PARAMS) + +module.exports = V1Api = + request: (options, callback) -> + return request(options) if !callback? + request options, (error, response, body) -> + return callback(error, response, body) if error? + if 200 <= response.statusCode < 300 or response.statusCode in (options.expectedStatusCodes or []) + callback null, response, body + else + error = new Error("overleaf v1 returned non-success code: #{response.statusCode}") + error.statusCode = response.statusCode + callback error diff --git a/services/web/app/coffee/infrastructure/Features.coffee b/services/web/app/coffee/infrastructure/Features.coffee index 37f62ddec2..7d47876773 100644 --- a/services/web/app/coffee/infrastructure/Features.coffee +++ b/services/web/app/coffee/infrastructure/Features.coffee @@ -9,7 +9,7 @@ module.exports = Features = when 'homepage' return Settings.enableHomepage when 'registration' - return not Features.externalAuthenticationSystemUsed() + return not Features.externalAuthenticationSystemUsed() or Settings.overleaf? when 'github-sync' return Settings.enableGithubSync when 'v1-return-message' diff --git a/services/web/app/coffee/infrastructure/RedirectManager.coffee b/services/web/app/coffee/infrastructure/RedirectManager.coffee index fe64f31c05..d4a9dd7c9a 100644 --- a/services/web/app/coffee/infrastructure/RedirectManager.coffee +++ b/services/web/app/coffee/infrastructure/RedirectManager.coffee @@ -1,5 +1,7 @@ settings = require("settings-sharelatex") logger = require("logger-sharelatex") +URL = require('url') +querystring = require('querystring') module.exports = RedirectManager = apply: (webRouter) -> @@ -13,14 +15,22 @@ module.exports = RedirectManager = if typeof target is 'string' url = target else - if req.method == "POST" + if req.method != "GET" code = 307 + if typeof target.url == "function" url = target.url(req.params) if !url return next() else url = target.url + + # Special handling for redirecting to v1, to ensure that query params + # are encoded + if target.authWithV1 + url = "/sign_in_to_v1?" + querystring.stringify(return_to: url + getQueryString(req)) + return res.redirect code, url + if target.baseUrl? url = "#{target.baseUrl}#{url}" res.redirect code, url + getQueryString(req) @@ -29,5 +39,5 @@ module.exports = RedirectManager = # have differences between Express and Rails, so safer to just pass the raw # string getQueryString = (req) -> - qs = req.url.match(/\?.*$/) - if qs? then qs[0] else "" + {search} = URL.parse(req.url) + if search then search else "" diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index ac0889d0c4..954356c4a0 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -90,8 +90,9 @@ module.exports = class Router if Settings.enableSubscriptions webRouter.get '/user/bonus', AuthenticationController.requireLogin(), ReferalController.bonus - webRouter.get '/blog', BlogController.getIndexPage - webRouter.get '/blog/*', BlogController.getPage + if !Settings.overleaf? + webRouter.get '/blog', BlogController.getIndexPage + webRouter.get '/blog/*', BlogController.getPage webRouter.get '/user/activate', UserPagesController.activateAccountPage AuthenticationController.addEndpointToLoginWhitelist '/user/activate' @@ -336,7 +337,7 @@ module.exports = class Router if AuthenticationController.isUserLoggedIn(req) res.redirect('/user/subscription') else - res.redirect("#{settings.v1Api.host}/teams") + res.redirect("#{settings.overleaf.host}/teams") webRouter.get '/chrome', (req, res, next) -> # Match v1 behaviour - this is used for a Chrome web app diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug index 6dc27a4241..b81d23904c 100644 --- a/services/web/app/views/project/editor/new_from_template.pug +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -24,3 +24,4 @@ block content input(type="hidden" name="templateVersionId" value=templateVersionId) input(type="hidden" name="templateName" value=name) input(type="hidden" name="compiler" value=compiler) + input(type="hidden" name="mainFile" value=mainFile) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 0fec00cb55..cee7d97efe 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -119,4 +119,4 @@ block content include ./list/modals - include ./list/front-chat + //- include ./list/front-chat diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 7ceb316b53..001178c5dd 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -204,6 +204,11 @@ define [ when options.try then "silent" # allow use to try compile once when $scope.stop_on_validation_error then "error" # try to compile else "silent" # ignore errors + # FIXME: Temporarily disable syntax checking as it is causing + # excessive support requests for projects migrated from v1 + # https://github.com/overleaf/sharelatex/issues/911 + if checkType == "error" + checkType = "silent" return $http.post url, { rootDoc_id: options.rootDocOverride_id or null draft: $scope.draft diff --git a/services/web/public/img/crests/agder.png b/services/web/public/img/crests/agder.png new file mode 100644 index 0000000000..f2102e7cf0 Binary files /dev/null and b/services/web/public/img/crests/agder.png differ diff --git a/services/web/public/img/crests/caltech.png b/services/web/public/img/crests/caltech.png new file mode 100644 index 0000000000..3200b8ed22 Binary files /dev/null and b/services/web/public/img/crests/caltech.png differ diff --git a/services/web/public/img/crests/queensland.png b/services/web/public/img/crests/queensland.png new file mode 100644 index 0000000000..9b83dd2a48 Binary files /dev/null and b/services/web/public/img/crests/queensland.png differ diff --git a/services/web/public/img/crests/york.png b/services/web/public/img/crests/york.png new file mode 100644 index 0000000000..0bd907081d Binary files /dev/null and b/services/web/public/img/crests/york.png differ diff --git a/services/web/public/stylesheets/app/blog.less b/services/web/public/stylesheets/app/blog.less index 1e474e54d5..190e2562db 100644 --- a/services/web/public/stylesheets/app/blog.less +++ b/services/web/public/stylesheets/app/blog.less @@ -14,6 +14,9 @@ } .blog { + iframe { + width: 100%; + } > .page-header { h1 { margin: 0; diff --git a/services/web/public/stylesheets/app/cms-page.less b/services/web/public/stylesheets/app/cms-page.less index 1e7f0aa4ed..bf5c2ff437 100644 --- a/services/web/public/stylesheets/app/cms-page.less +++ b/services/web/public/stylesheets/app/cms-page.less @@ -45,8 +45,8 @@ All content from CMS is in .row-spaced. Margin below is to fix extra whitespace for first rows */ - .container > .row:nth-child(2) { - //- first child is page header, don't correct margin + .container > .row:nth-child(2), .content-container > .row:first-child { + //- .container first child is page header, don't correct margin margin-top: 0; } .tab-pane { diff --git a/services/web/public/stylesheets/app/contact-us.less b/services/web/public/stylesheets/app/contact-us.less index 37577f60e0..caa8cad5f6 100644 --- a/services/web/public/stylesheets/app/contact-us.less +++ b/services/web/public/stylesheets/app/contact-us.less @@ -43,7 +43,7 @@ &:hover, &:focus { text-decoration: none; - color: @dropdown-link-hover-color; + color: @dropdown-link-hover-color!important; background-color: @dropdown-link-hover-bg; .fa { diff --git a/services/web/public/stylesheets/app/content_page.less b/services/web/public/stylesheets/app/content_page.less index 6e6374d944..1b462ff996 100644 --- a/services/web/public/stylesheets/app/content_page.less +++ b/services/web/public/stylesheets/app/content_page.less @@ -5,5 +5,14 @@ .content-page { a:not(.btn) { color: @link-color-alt; + &:hover { + color: @link-hover-color-alt; + } + } + hr { + border-color: @hr-border-alt; + } + .quote-by { + overflow: hidden; } } \ No newline at end of file diff --git a/services/web/public/stylesheets/app/portals.less b/services/web/public/stylesheets/app/portals.less index 5e987f9d58..7ef506c6a0 100644 --- a/services/web/public/stylesheets/app/portals.less +++ b/services/web/public/stylesheets/app/portals.less @@ -90,6 +90,10 @@ } // End Actions + .nav-tabs { + margin-bottom: @margin-md; + } + /* Begin Print */ diff --git a/services/web/public/stylesheets/components/tabs.less b/services/web/public/stylesheets/components/tabs.less index 71143cbefe..0a942c1e59 100644 --- a/services/web/public/stylesheets/components/tabs.less +++ b/services/web/public/stylesheets/components/tabs.less @@ -2,26 +2,23 @@ // Overrides for nav.less .nav-tabs { border: 0!important; - margin-bottom: @margin-md; + margin-bottom: 0; margin-top: -@line-height-computed; //- adjusted for portal-name padding: @padding-lg 0 @padding-md; text-align: center; - - a { - color: @link-color; - &:hover { - background-color: transparent!important; - border: 0!important; - color: @link-hover-color!important; - } - } } - li { + .nav-tabs > li { display: inline-block; float: none; a { border: 0; + color: @link-color-alt; + &:hover { + background-color: transparent!important; + border: 0!important; + color: @link-hover-color-alt; + } } } @@ -39,6 +36,4 @@ background-color: transparent!important; border: none!important; } -} - - \ No newline at end of file +} \ No newline at end of file diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 6bf5284ad5..fcef0e67f4 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -52,7 +52,9 @@ @link-color-alt : @ol-green; @link-active-color : @ol-dark-green; @link-hover-color : @ol-dark-blue; +@link-hover-color-alt : @ol-dark-green; @hr-border : @ol-blue-gray-1; +@hr-border-alt : @gray-lighter; // Button colors and sizing @btn-border-width : 0; diff --git a/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee b/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee index 7cd9ecfd22..71a6902c81 100644 --- a/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee +++ b/services/web/test/acceptance/coffee/RedirectUrlsTests.coffee @@ -31,4 +31,13 @@ describe "RedirectUrls", -> assertRedirect 'get', '/redirect/get_and_post', 302, '/destination/get_and_post', done it 'redirects with query params', (done) -> - assertRedirect 'get', '/redirect/qs?foo=bar&baz[]=qux1&baz[]=qux2', 302, '/destination/qs?foo=bar&baz[]=qux1&baz[]=qux2', done \ No newline at end of file + assertRedirect 'get', '/redirect/qs?foo=bar&baz[]=qux1&baz[]=qux2', 302, '/destination/qs?foo=bar&baz[]=qux1&baz[]=qux2', done + + it 'redirects to /sign_in_to_v1 with authWithV1 setting', (done) -> + assertRedirect( + 'get', + '/docs?zip_uri=http%3A%2F%2Foverleaf.test%2Ffoo%3Fbar%3Dbaz%26qux%3Dthing&bar=baz', + 302, + '/sign_in_to_v1?return_to=%2Fdocs%3Fzip_uri%3Dhttp%253A%252F%252Foverleaf.test%252Ffoo%253Fbar%253Dbaz%2526qux%253Dthing%26bar%3Dbaz', + done + ) \ No newline at end of file diff --git a/services/web/test/acceptance/coffee/TokenAccessTests.coffee b/services/web/test/acceptance/coffee/TokenAccessTests.coffee index b0fbd7a0a1..832281f6a3 100644 --- a/services/web/test/acceptance/coffee/TokenAccessTests.coffee +++ b/services/web/test/acceptance/coffee/TokenAccessTests.coffee @@ -417,11 +417,20 @@ describe 'TokenAccess', -> , done) describe 'unimported v1 project', -> - it 'should redirect to v1', (done) -> + it 'should redirect read and write token to v1', (done) -> unimportedV1Token = '123abc' try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) => expect(response.statusCode).to.equal 302 expect(response.headers.location).to.equal( - 'http://overleaf.test:5000/123abc' + '/sign_in_to_v1?return_to=/123abc' + ) + , done) + + it 'should redirect read only token to v1', (done) -> + unimportedV1Token = 'abcd' + try_read_only_token_access(@owner, unimportedV1Token, (response, body) => + expect(response.statusCode).to.equal 302 + expect(response.headers.location).to.equal( + '/sign_in_to_v1?return_to=/read/abcd' ) , done) diff --git a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee index eeafa6a44b..fcaf7a80df 100644 --- a/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockV1Api.coffee @@ -81,5 +81,11 @@ module.exports = MockV1Api = .on "error", (error) -> console.error "error starting MockV1Api:", error.message process.exit(1) + + app.get '/api/v1/sharelatex/docs/:token/is_published', (req, res, next) => + res.json { allow: true } + + app.get '/api/v1/sharelatex/docs/:token/exported_to_v2', (req, res, next) => + res.json { exported: false } MockV1Api.run() diff --git a/services/web/test/acceptance/config/settings.test.coffee b/services/web/test/acceptance/config/settings.test.coffee index 0890823ab7..893af4dde3 100644 --- a/services/web/test/acceptance/config/settings.test.coffee +++ b/services/web/test/acceptance/config/settings.test.coffee @@ -128,3 +128,7 @@ module.exports = url: (params) -> "/destination/#{params.id}/params" }, '/redirect/qs': '/destination/qs' + '/docs': { + authWithV1: true + url: '/docs' + } diff --git a/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee b/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee index 9e1b437bd0..dfa21508d8 100644 --- a/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee +++ b/services/web/test/unit/coffee/Downloads/ProjectDownloadsControllerTests.coffee @@ -112,7 +112,7 @@ describe "ProjectDownloadsController", -> @res.setContentDisposition .calledWith( 'attachment', - {filename: "ShareLaTeX Projects (2 items).zip"}) + {filename: "Overleaf Projects (2 items).zip"}) .should.equal true it "should record the action via Metrics", -> diff --git a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee index 3b47c48420..0dd7d11ac8 100644 --- a/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectDetailsHandlerTests.coffee @@ -155,6 +155,62 @@ describe 'ProjectDetailsHandler', -> expect(error).to.not.exist done() + describe "ensureProjectNameIsUnique", -> + beforeEach -> + @result = { + owned: ["name", "name1", "name11"] + readAndWrite: ["name2", "name22"] + readOnly: ["name3", "name33"] + tokenReadAndWrite: ["name4", "name44"] + tokenReadOnly: ["name5", "name55", "x".repeat(15)] + } + @ProjectGetter.findAllUsersProjects = sinon.stub().callsArgWith(2, null, @result) + + it "should leave a unique name unchanged", (done) -> + @handler.ensureProjectNameIsUnique @user_id, "unique-name", ["-test-suffix"], (error, name, changed) -> + expect(name).to.equal "unique-name" + expect(changed).to.equal false + done() + + it "should append a suffix to an existing name", (done) -> + @handler.ensureProjectNameIsUnique @user_id, "name1", ["-test-suffix"], (error, name, changed) -> + expect(name).to.equal "name1-test-suffix" + expect(changed).to.equal true + done() + + it "should fallback to a second suffix when needed", (done) -> + @handler.ensureProjectNameIsUnique @user_id, "name1", ["1", "-test-suffix"], (error, name, changed) -> + expect(name).to.equal "name1-test-suffix" + expect(changed).to.equal true + done() + + it "should truncate the name when append a suffix if the result is too long", (done) -> + @handler.MAX_PROJECT_NAME_LENGTH = 20 + @handler.ensureProjectNameIsUnique @user_id, "x".repeat(15), ["-test-suffix"], (error, name, changed) -> + expect(name).to.equal "x".repeat(8) + "-test-suffix" + expect(changed).to.equal true + done() + + it "should return an error if the name cannot be made unique", (done) -> + @handler.ensureProjectNameIsUnique @user_id, "name", ["1", "5", "55"], (error, name, changed) -> + expect(error).to.eql new Errors.InvalidNameError("Project name could not be made unique") + done() + + describe "fixProjectName", -> + + it "should change empty names to Untitled", () -> + expect(@handler.fixProjectName "").to.equal "Untitled" + + it "should replace / with -", () -> + expect(@handler.fixProjectName "foo/bar").to.equal "foo-bar" + + it "should truncate long names", () -> + expect(@handler.fixProjectName new Array(1000).join("a")).to.equal "a".repeat(150) + + it "should accept normal names", () -> + expect(@handler.fixProjectName "foobar").to.equal "foobar" + + describe "setPublicAccessLevel", -> beforeEach -> @ProjectModel.update.callsArgWith(2) diff --git a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee index 9a8cde3ff5..47c2973e9f 100644 --- a/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectRootDocManagerTests.coffee @@ -75,3 +75,93 @@ describe 'ProjectRootDocManager', -> it "should not set the root doc to the doc containing a documentclass", -> @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false + describe "setRootDocFromName", -> + describe "when there is a suitable root doc", -> + beforeEach (done)-> + @docPaths = + "doc-id-1": "/chapter1.tex" + "doc-id-2": "/main.tex" + "doc-id-3": "/nested/chapter1a.tex" + "doc-id-4": "/nested/chapter1b.tex" + @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectRootDocManager.setRootDocFromName @project_id, '/main.tex', done + + it "should check the docs of the project", -> + @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) + .should.equal true + + it "should set the root doc to main.tex", -> + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") + .should.equal true + + describe "when there is a suitable root doc but the leading slash is missing", -> + beforeEach (done)-> + @docPaths = + "doc-id-1": "/chapter1.tex" + "doc-id-2": "/main.tex" + "doc-id-3": "/nested/chapter1a.tex" + "doc-id-4": "/nested/chapter1b.tex" + @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectRootDocManager.setRootDocFromName @project_id, 'main.tex', done + + it "should check the docs of the project", -> + @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) + .should.equal true + + it "should set the root doc to main.tex", -> + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") + .should.equal true + + describe "when there is a suitable root doc with a basename match", -> + beforeEach (done)-> + @docPaths = + "doc-id-1": "/chapter1.tex" + "doc-id-2": "/main.tex" + "doc-id-3": "/nested/chapter1a.tex" + "doc-id-4": "/nested/chapter1b.tex" + @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectRootDocManager.setRootDocFromName @project_id, 'chapter1a.tex', done + + it "should check the docs of the project", -> + @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) + .should.equal true + + it "should set the root doc using the basename", -> + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-3") + .should.equal true + + describe "when there is a suitable root doc but the filename is in quotes", -> + beforeEach (done)-> + @docPaths = + "doc-id-1": "/chapter1.tex" + "doc-id-2": "/main.tex" + "doc-id-3": "/nested/chapter1a.tex" + "doc-id-4": "/nested/chapter1b.tex" + @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectRootDocManager.setRootDocFromName @project_id, "'main.tex'", done + + it "should check the docs of the project", -> + @ProjectEntityHandler.getAllDocPathsFromProjectById.calledWith(@project_id) + .should.equal true + + it "should set the root doc to main.tex", -> + @ProjectEntityUpdateHandler.setRootDoc.calledWith(@project_id, "doc-id-2") + .should.equal true + + describe "when there is no suitable root doc", -> + beforeEach (done)-> + @docPaths = + "doc-id-1": "/chapter1.tex" + "doc-id-2": "/main.tex" + "doc-id-3": "/nested/chapter1a.tex" + "doc-id-4": "/nested/chapter1b.tex" + @ProjectEntityHandler.getAllDocPathsFromProjectById = sinon.stub().callsArgWith(1, null, @docPaths) + @ProjectEntityUpdateHandler.setRootDoc = sinon.stub().callsArgWith(2) + @ProjectRootDocManager.setRootDocFromName @project_id, "other.tex", done + + it "should not set the root doc", -> + @ProjectEntityUpdateHandler.setRootDoc.called.should.equal false diff --git a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee index e6f404516e..40cb18e41f 100644 --- a/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee +++ b/services/web/test/unit/coffee/Subscription/TeamInvitesHandlerTests.coffee @@ -176,6 +176,27 @@ describe "TeamInvitesHandler", -> ).should.equal true done() + it "stripe licence from name", (done) -> + @licence.name = 'Foo Licence' + @TeamInvitesHandler.createDomainInvite @user, @licence, (err, invite) => + @EmailHandler.sendEmail.calledWith("verifyEmailToJoinTeam", + sinon.match({ + inviterName: 'Foo' + }) + ).should.equal true + done() + + + it "stripe site licence from name", (done) -> + @licence.name = 'Foo Site Licence' + @TeamInvitesHandler.createDomainInvite @user, @licence, (err, invite) => + @EmailHandler.sendEmail.calledWith("verifyEmailToJoinTeam", + sinon.match({ + inviterName: 'Foo' + }) + ).should.equal true + done() + describe "importInvite", -> beforeEach -> @sentAt = new Date() diff --git a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee index 5cf52eca39..a08789ede9 100644 --- a/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee +++ b/services/web/test/unit/coffee/Templates/TemplatesControllerTests.coffee @@ -22,15 +22,24 @@ describe 'TemplatesController', -> } @ProjectUploadManager = {createProjectFromZipArchive : sinon.stub().callsArgWith(3, null, {_id:project_id})} @dumpFolder = "dump/path" - @ProjectOptionsHandler = {setCompiler:sinon.stub().callsArgWith(2)} + @ProjectOptionsHandler = { + setCompiler:sinon.stub().callsArgWith(2) + setImageName:sinon.stub().callsArgWith(2) + } @uuid = "1234" + @ProjectRootDocManager = { + setRootDocFromName: sinon.stub().callsArgWith(2) + } @ProjectDetailsHandler = getProjectDescription:sinon.stub() + fixProjectName: sinon.stub().returns(@templateName) @Project = update: sinon.stub().callsArgWith(3, null) @controller = SandboxedModule.require modulePath, requires: '../../../js/Features/Uploads/ProjectUploadManager':@ProjectUploadManager '../../../js/Features/Project/ProjectOptionsHandler':@ProjectOptionsHandler + '../../../js/Features/Project/ProjectRootDocManager':@ProjectRootDocManager + '../../../js/Features/Project/ProjectDetailsHandler':@ProjectDetailsHandler '../../../js/Features/Authentication/AuthenticationController': @AuthenticationController = {getLoggedInUserId: sinon.stub()} './TemplatesPublisher':@TemplatesPublisher "logger-sharelatex": diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee index 0bdb6d59e6..6d9728536d 100644 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee +++ b/services/web/test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee @@ -34,6 +34,9 @@ describe "TokenAccessController", -> overleaf: host: 'http://overleaf.test:5000' } + '../V1/V1Api': @V1Api = { + request: sinon.stub().callsArgWith(1, null, {}, { allow: true }) + } @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@userId.toString()) @@ -48,7 +51,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -85,7 +88,7 @@ describe "TokenAccessController", -> @req.params['read_and_write_token'] = @readAndWriteToken @project.owner_ref = @userId @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -123,7 +126,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -159,7 +162,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -244,17 +247,31 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = '123abc' @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, @project, false) - @TokenAccessController.readAndWriteToken @req, @res, @next + .callsArgWith(1, null, null, false) - it 'should redirect to v1', (done) -> - expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.firstCall.args[0]) - .to.equal 'http://overleaf.test:5000/123abc' - done() + describe 'when project was not exported from v1', -> + beforeEach -> + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, false) + @TokenAccessController.readAndWriteToken @req, @res, @next + + it 'should redirect to v1', (done) -> + expect(@res.redirect.callCount).to.equal 1 + expect(@res.redirect.calledWith( + 302, + '/sign_in_to_v1?return_to=/123abc' + )).to.equal true + done() + + describe 'when project was exported from v1', -> + beforeEach -> + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, false) + @TokenAccessController.readAndWriteToken @req, @res, @next + + it 'should call next with a not-found error', (done) -> + expect(@next.callCount).to.equal 0 + done() describe 'when token access is off, but user has higher access anyway', -> beforeEach -> @@ -264,10 +281,10 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) + .callsArgWith(1, null, null, true) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, @project, true) + .callsArgWith(2, null, @project) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -313,10 +330,10 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) + .callsArgWith(1, null, null, true) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, null, true) + .callsArgWith(2, null, null) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -358,7 +375,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, new Error('woops')) @ProjectController.loadEditor = sinon.stub() @@ -393,6 +410,21 @@ describe "TokenAccessController", -> describe 'readOnlyToken', -> beforeEach -> + @TokenAccessHandler.checkV1Access = sinon.stub().callsArgWith(1, null, true) + + describe 'when access not allowed by v1 api', -> + beforeEach -> + @req = new MockRequest() + @res = new MockResponse() + @res.redirect = sinon.stub() + @next = sinon.stub() + @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() + .callsArgWith(1, null, @project, true) + @TokenAccessHandler.checkV1Access = sinon.stub().callsArgWith(1, null, false, 'doc-url') + @TokenAccessController.readOnlyToken @req, @res, @next + + it 'should redirect to doc-url', -> + expect(@res.redirect.calledWith('doc-url')).to.equal true describe 'with a user', -> beforeEach -> @@ -405,7 +437,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_only_token'] = @readOnlyToken @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -441,7 +473,7 @@ describe "TokenAccessController", -> @req.params['read_only_token'] = @readOnlyToken @project.owner_ref = @userId @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -500,29 +532,43 @@ describe "TokenAccessController", -> expect(@next.lastCall.args[0]).to.be.instanceof Error done() - ## describe 'when findProject does not find a project', -> - beforeEach -> - describe 'when project does not exist', -> beforeEach -> @req = new MockRequest() - @req.url = '/123abc' @res = new MockResponse() @res.redirect = sinon.stub() @next = sinon.stub() - @req.params['read_and_write_token'] = '123abc' + @req.params['read_only_token'] = 'abcd' @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, null) - @TokenAccessHandler.findProjectWithHigherAccess = - sinon.stub() - .callsArgWith(2, null, @project, false) + .callsArgWith(1, null, null, false) + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, false) @TokenAccessController.readOnlyToken @req, @res, @next - it 'should return a ProjectNotTokenAccessError', (done) -> + it 'should redirect to v1', (done) -> expect(@res.redirect.callCount).to.equal 1 - expect(@res.redirect.firstCall.args[0]) - .to.equal 'http://overleaf.test:5000/123abc' + expect(@res.redirect.calledWith( + 302, + '/sign_in_to_v1?return_to=/read/abcd' + )).to.equal true + done() + + describe 'when project was exported from v1', -> + beforeEach -> + @req = new MockRequest() + @res = new MockResponse() + @res.redirect = sinon.stub() + @next = sinon.stub() + @req.params['read_only_token'] = 'abcd' + @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() + .callsArgWith(1, null, null, false) + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, true) + @TokenAccessController.readOnlyToken @req, @res, @next + + it 'should call next with a not-found error', (done) -> + expect(@next.callCount).to.equal 1 done() describe 'when token access is off, but user has higher access anyway', -> @@ -533,10 +579,10 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) + .callsArgWith(1, null, null, true) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, @project, true) + .callsArgWith(2, null, @project) @TokenAccessHandler.addReadAndWriteUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -581,10 +627,10 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_and_write_token'] = @readAndWriteToken @TokenAccessHandler.findProjectWithReadAndWriteToken = sinon.stub() - .callsArgWith(1, null, null) + .callsArgWith(1, null, null, true) @TokenAccessHandler.findProjectWithHigherAccess = sinon.stub() - .callsArgWith(2, null, null, true) + .callsArgWith(2, null, null) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -626,7 +672,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_only_token'] = @readOnlyToken @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, new Error('woops')) @ProjectController.loadEditor = sinon.stub() @@ -670,7 +716,7 @@ describe "TokenAccessController", -> @next = sinon.stub() @req.params['read_only_token'] = @readOnlyToken @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() - .callsArgWith(1, null, @project) + .callsArgWith(1, null, @project, true) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -748,10 +794,13 @@ describe "TokenAccessController", -> beforeEach -> @req = new MockRequest() @res = new MockResponse() + @res.redirect = sinon.stub() @next = sinon.stub() @req.params['read_only_token'] = @readOnlyToken @TokenAccessHandler.findProjectWithReadOnlyToken = sinon.stub() .callsArgWith(1, null, null) + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, false) @TokenAccessHandler.addReadOnlyUserToProject = sinon.stub() .callsArgWith(2, null) @ProjectController.loadEditor = sinon.stub() @@ -779,8 +828,17 @@ describe "TokenAccessController", -> .to.equal 0 done() - it 'should call next with a not-found error', (done) -> - expect(@next.callCount).to.equal 1 - expect(@next.lastCall.args[0]).to.be.instanceof Error - done() + describe 'when project was exported to v2', -> + beforeEach -> + @TokenAccessHandler.checkV1ProjectExported = sinon.stub() + .callsArgWith(1, null, true) + @TokenAccessController.readOnlyToken @req, @res, @next + + it 'should redirect to v1', (done) -> + expect(@res.redirect.callCount).to.equal 1 + expect(@res.redirect.calledWith( + 302, + "/sign_in_to_v1?return_to=/read/#{@readOnlyToken}" + )).to.equal true + done() diff --git a/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee b/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee index 722a8496a2..730325204a 100644 --- a/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee +++ b/services/web/test/unit/coffee/TokenAccess/TokenAccessHandlerTests.coffee @@ -19,9 +19,11 @@ describe "TokenAccessHandler", -> @req = {} @TokenAccessHandler = SandboxedModule.require modulePath, requires: '../../models/Project': {Project: @Project = {}} - 'settings-sharelatex': {} + 'settings-sharelatex': @settings = {} '../Collaborators/CollaboratorsHandler': @CollaboratorsHandler = {} - + '../V1/V1Api': @V1Api = { + request: sinon.stub() + } describe 'findProjectWithReadOnlyToken', -> beforeEach -> @@ -31,8 +33,7 @@ describe "TokenAccessHandler", -> @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) => expect(@Project.findOne.callCount).to.equal 1 expect(@Project.findOne.calledWith({ - 'tokens.readOnly': @token, - 'publicAccesLevel': 'tokenBased' + 'tokens.readOnly': @token })).to.equal true done() @@ -43,6 +44,11 @@ describe "TokenAccessHandler", -> expect(project).to.deep.equal @project done() + it 'should return projectExists flag as true', (done) -> + @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> + expect(projectExists).to.equal true + done() + describe 'when Project.findOne produces an error', -> beforeEach -> @Project.findOne = sinon.stub().callsArgWith(2, new Error('woops')) @@ -54,6 +60,37 @@ describe "TokenAccessHandler", -> expect(err).to.be.instanceof Error done() + describe 'when project does not have tokenBased access level', -> + beforeEach -> + @project.publicAccesLevel = 'private' + @Project.findOne = sinon.stub().callsArgWith(2, null, @project, true) + + it 'should not return a project', (done) -> + @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) -> + expect(err).to.not.exist + expect(project).to.not.exist + done() + + it 'should return projectExists flag as true', (done) -> + @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> + expect(projectExists).to.equal true + done() + + describe 'when project does not exist', -> + beforeEach -> + @Project.findOne = sinon.stub().callsArgWith(2, null, null) + + it 'should not return a project', (done) -> + @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project) -> + expect(err).to.not.exist + expect(project).to.not.exist + done() + + it 'should return projectExists flag as false', (done) -> + @TokenAccessHandler.findProjectWithReadOnlyToken @token, (err, project, projectExists) -> + expect(projectExists).to.equal false + done() + describe 'findProjectWithReadAndWriteToken', -> beforeEach -> @Project.findOne = sinon.stub().callsArgWith(2, null, @project) @@ -62,8 +99,7 @@ describe "TokenAccessHandler", -> @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) => expect(@Project.findOne.callCount).to.equal 1 expect(@Project.findOne.calledWith({ - 'tokens.readAndWrite': @token, - 'publicAccesLevel': 'tokenBased' + 'tokens.readAndWrite': @token })).to.equal true done() @@ -74,6 +110,11 @@ describe "TokenAccessHandler", -> expect(project).to.deep.equal @project done() + it 'should return projectExists flag as true', (done) -> + @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project, projectExists) -> + expect(projectExists).to.equal true + done() + describe 'when Project.findOne produces an error', -> beforeEach -> @Project.findOne = sinon.stub().callsArgWith(2, new Error('woops')) @@ -85,6 +126,22 @@ describe "TokenAccessHandler", -> expect(err).to.be.instanceof Error done() + describe 'when project does not have tokenBased access level', -> + beforeEach -> + @project.publicAccesLevel = 'private' + @Project.findOne = sinon.stub().callsArgWith(2, null, @project, true) + + it 'should not return a project', (done) -> + @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project) -> + expect(err).to.not.exist + expect(project).to.not.exist + done() + + it 'should return projectExists flag as true', (done) -> + @TokenAccessHandler.findProjectWithReadAndWriteToken @token, (err, project, projectExists) -> + expect(projectExists).to.equal true + done() + describe 'findProjectWithHigherAccess', -> describe 'when user does have higher access', -> @@ -434,3 +491,46 @@ describe "TokenAccessHandler", -> @TokenAccessHandler.protectTokens(@project, 'owner') expect(@project.tokens.readAndWrite).to.equal 'rw' expect(@project.tokens.readOnly).to.equal 'ro' + + describe 'checkV1Access', -> + beforeEach -> + @callback = sinon.stub() + + describe 'when v1 api not set', -> + beforeEach -> + @TokenAccessHandler.checkV1Access @token, @callback + + it 'should not check access and return true', -> + expect(@V1Api.request.called).to.equal false + expect(@callback.calledWith null, true).to.equal true + + describe 'when v1 api is set', -> + beforeEach -> + @settings.apis = { v1: 'v1' } + + describe 'when access allowed', -> + beforeEach -> + @V1Api.request = sinon.stub().callsArgWith(1, null, {}, { allow: true} ) + @TokenAccessHandler.checkV1Access @token, @callback + + it 'should check api', -> + expect(@V1Api.request.calledWith { url: "/api/v1/sharelatex/docs/#{@token}/is_published" }).to.equal true + + it 'should callback with true', -> + expect(@callback.calledWith null, true).to.equal true + + describe 'when access denied', -> + beforeEach -> + @V1Api.request = sinon.stub().callsArgWith(1, null, {}, { allow: false, published_path: 'doc-url'} ) + @TokenAccessHandler.checkV1Access @token, @callback + + it 'should callback with false and redirect', -> + expect(@callback.calledWith null, false, 'doc-url').to.equal true + + describe 'on error', -> + beforeEach -> + @V1Api.request = sinon.stub().callsArgWith(1, 'error') + @TokenAccessHandler.checkV1Access @token, @callback + + it 'should callback with error', -> + expect(@callback.calledWith 'error').to.equal true