Merge branch 'master' into pr-user-affilitations

This commit is contained in:
Paulo Reis
2018-06-25 14:13:26 +01:00
72 changed files with 2792 additions and 1012 deletions
+11 -6
View File
@@ -196,8 +196,11 @@ test_frontend: test_clean # stop service
test_acceptance: test_acceptance_app test_acceptance_modules
test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run
$(MAKE) test_acceptance_app_stop_service
test_acceptance_app:
@set -e; \
$(MAKE) test_acceptance_app_start_service; \
$(MAKE) test_acceptance_app_run; \
$(MAKE) test_acceptance_app_stop_service;
test_acceptance_app_start_service: test_clean # stop service and clear dbs
$(MAKE) compile
@@ -208,10 +211,12 @@ test_acceptance_app_stop_service:
test_acceptance_app_run:
@docker-compose ${DOCKER_COMPOSE_FLAGS} exec -T test_acceptance npm -q run test:acceptance -- ${MOCHA_ARGS}; \
if [ $$? -eq 137 ]; then \
echo "\nOh dear, it looks like the web process crashed! To see why, run:\n\n\tdocker-compose logs test_acceptance\n"; \
exit 1; \
fi
result=$$?; \
if [ $$result -eq 137 ]; then \
docker-compose logs --tail=50 test_acceptance; \
echo "\nOh dear, it looks like the web process crashed! Some logs are above, but to see them all, run:\n\n\tdocker-compose logs test_acceptance\n"; \
fi; \
exit $$result
test_acceptance_modules:
@set -e; \
@@ -14,6 +14,7 @@ async = require("async")
ClsiFormatChecker = require("./ClsiFormatChecker")
DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler"
Metrics = require('metrics-sharelatex')
Errors = require ('../Errors/Errors')
module.exports = ClsiManager =
@@ -35,24 +36,16 @@ module.exports = ClsiManager =
else
return callback(error)
logger.log project_id: project_id, "sending compile to CLSI"
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
if err?
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
return callback(err)
if validationProblems?
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
return callback(null, "validation-problems", null, null, validationProblems)
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
if error?
logger.err err:error, project_id:project_id, "error sending request to clsi"
return callback(error)
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
if err?
logger.err err:err, project_id:project_id, "error getting server id"
return callback(err)
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
callback(null, response?.compile?.status, outputFiles, clsiServerId)
ClsiManager._sendBuiltRequest project_id, user_id, req, options, (error, status, result...) ->
return callback(error) if error?
callback(error, status, result...)
# for public API requests where there is no project id
sendExternalRequest: (submission_id, clsi_request, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
logger.log submission_id: submission_id, "sending external compile to CLSI", clsi_request
ClsiManager._sendBuiltRequest submission_id, null, clsi_request, options, (error, status, result...) ->
return callback(error) if error?
callback(error, status, result...)
stopCompile: (project_id, user_id, options, callback = (error) ->) ->
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")
@@ -74,6 +67,26 @@ module.exports = ClsiManager =
return callback(error) if error?
callback()
_sendBuiltRequest: (project_id, user_id, req, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
if err?
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
return callback(err)
if validationProblems?
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
return callback(null, "validation-problems", null, null, validationProblems)
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
if error?
logger.err err:error, project_id:project_id, "error sending request to clsi"
return callback(error)
logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
if err?
logger.err err:err, project_id:project_id, "error getting server id"
return callback(err)
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
callback(null, response?.compile?.status, outputFiles, clsiServerId)
_makeRequest: (project_id, opts, callback)->
ClsiCookieManager.getCookieJar project_id, (err, jar)->
if err?
@@ -87,7 +100,7 @@ module.exports = ClsiManager =
ClsiCookieManager.setServerId project_id, response, (err)->
if err?
logger.warn err:err, project_id:project_id, "error setting server id"
return callback err, response, body
_getCompilerUrl: (compileGroup, project_id, user_id, action) ->
@@ -169,6 +182,14 @@ module.exports = ClsiManager =
return callback(error) if error?
callback(null, projectStateHash, docs)
getOutputFileStream: (project_id, user_id, build_id, output_file_path, callback=(err, readStream)->) ->
url = "#{Settings.apis.clsi.url}/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{output_file_path}"
ClsiCookieManager.getCookieJar project_id, (err, jar)->
return callback(err) if err?
options = { url: url, method: "GET", timeout: 60 * 1000, jar : jar }
readStream = request(options)
callback(null, readStream)
_buildRequestFromDocupdater: (project_id, options, project, projectStateHash, docUpdaterDocs, callback = (error, request) ->) ->
ProjectEntityHandler.getAllDocPathsFromProject project, (error, docPath) ->
return callback(error) if error?
@@ -55,6 +55,33 @@ module.exports = CompileController =
return next(error) if error?
res.status(200).send()
# Used for submissions through the public API
compileSubmission: (req, res, next = (error) ->) ->
res.setTimeout(5 * 60 * 1000)
submission_id = req.params.submission_id
options = {}
if req.body?.rootResourcePath?
options.rootResourcePath = req.body.rootResourcePath
if req.body?.compiler
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.body?.check in ['validate', 'error', 'silent']
options.check = req.body.check
options.compileGroup = req.body?.compileGroup || Settings.defaultFeatures.compileGroup
options.timeout = req.body?.timeout || Settings.defaultFeatures.compileTimeout
logger.log {options:options, submission_id:submission_id}, "got compileSubmission request"
ClsiManager.sendExternalRequest submission_id, req.body, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
return next(error) if error?
logger.log {submission_id:submission_id, files:outputFiles}, "compileSubmission output files"
res.contentType("application/json")
res.status(200).send JSON.stringify {
status: status
outputFiles: outputFiles
clsiServerId: clsiServerId
validationProblems: validationProblems
}
_compileAsUser: (req, callback) ->
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
@@ -139,6 +166,12 @@ module.exports = CompileController =
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file
CompileController.proxyToClsi(project_id, url, req, res, next)
getFileFromClsiWithoutUser: (req, res, next = (error) ->) ->
submission_id = req.params.submission_id
url = CompileController._getFileUrl submission_id, null, req.params.build_id, req.params.file
limits = { compileGroup: req.body?.compileGroup || Settings.defaultFeatures.compileGroup }
CompileController.proxyToClsiWithLimits(submission_id, url, limits, req, res, next)
# compute a GET file url for a given project, user (optional), build (optional) and file
_getFileUrl: (project_id, user_id, build_id, file) ->
if user_id? and build_id?
@@ -4,13 +4,17 @@ settings = require "settings-sharelatex"
module.exports = _.template """
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
<h3 class="avoid-auto-linking" style="Margin: 0; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
<%= title %>
</h3>
<% if (title) { %>
<h3 class="avoid-auto-linking" style="Margin: 0; color: inherit; font-family: Baskerville, 'Baskerville Old Face', Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
<%= title %>
</h3>
<% } %>
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= greeting %>
</p>
<% if (greeting) { %>
<p style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= greeting %>
</p>
<% } %>
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #0a0a0a; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= message %>
</p>
@@ -4,13 +4,17 @@ settings = require "settings-sharelatex"
module.exports = _.template """
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: table; padding: 0; position: relative; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;">
<th class="small-12 large-12 columns first last" style="Margin: 0 auto; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0 auto; padding: 0; padding-bottom: 16px; padding-left: 16px; padding-right: 16px; text-align: left; width: 564px;"><table style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tr style="padding: 0; text-align: left; vertical-align: top;"><th style="Margin: 0; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left;">
<h3 class="avoid-auto-linking" style="Margin: 0; color: #5D6879; font-family: Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
<%= title %>
</h3>
<% if (title) { %>
<h3 class="avoid-auto-linking" style="Margin: 0; color: #5D6879; font-family: Georgia, serif; font-size: 24px; font-weight: normal; line-height: 1.3; margin: 0; padding: 0; text-align: left; word-wrap: normal;">
<%= title %>
</h3>
<% } %>
<table class="spacer" style="border-collapse: collapse; border-spacing: 0; padding: 0; text-align: left; vertical-align: top; width: 100%;"><tbody><tr style="padding: 0; text-align: left; vertical-align: top;"><td height="20px" style="-moz-hyphens: auto; -webkit-hyphens: auto; Margin: 0; border-collapse: collapse !important; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 20px; font-weight: normal; hyphens: auto; line-height: 20px; margin: 0; mso-line-height-rule: exactly; padding: 0; text-align: left; vertical-align: top; word-wrap: break-word;">&#xA0;</td></tr></tbody></table>
<p style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= greeting %>
</p>
<% if (greeting) { %>
<p style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= greeting %>
</p>
<% } %>
<p class="avoid-auto-linking" style="Margin: 0; Margin-bottom: 10px; color: #5D6879; font-family: Helvetica, Arial, sans-serif; font-size: 16px; font-weight: normal; line-height: 1.3; margin: 0; margin-bottom: 10px; padding: 0; text-align: left;">
<%= message %>
</p>
@@ -1,5 +1,6 @@
_ = require('underscore')
settings = require("settings-sharelatex")
marked = require('marked')
PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
@@ -7,169 +8,113 @@ BaseWithHeaderEmailLayout = require("./Layouts/" + settings.brandPrefix + "BaseW
SingleCTAEmailBody = require("./Bodies/" + settings.brandPrefix + "SingleCTAEmailBody")
CTAEmailTemplate = (content) ->
content.greeting ?= () -> 'Hi,'
content.secondaryMessage ?= () -> ""
return {
subject: (opts) -> content.subject(opts),
layout: BaseWithHeaderEmailLayout,
plainTextTemplate: (opts) -> """
#{content.greeting(opts)}
#{content.message(opts).trim()}
#{content.ctaText(opts)}: #{content.ctaURL(opts)}
#{content.secondaryMessage?(opts).trim() or ""}
Regards,
The #{settings.appName} Team - #{settings.siteUrl}
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: content.title?(opts)
greeting: content.greeting(opts)
message: marked(content.message(opts).trim())
secondaryMessage: marked(content.secondaryMessage(opts).trim())
ctaText: content.ctaText(opts)
ctaURL: content.ctaURL(opts)
gmailGoToAction: content.gmailGoToAction?(opts)
})
}
templates = {}
templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
plainTextTemplate: _.template """
Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
templates.registered = CTAEmailTemplate({
subject: () -> "Activate your #{settings.appName} Account"
message: (opts) -> """
Congratulations, you've just had an account created for you on #{settings.appName} with the email address '#{opts.to}'.
Click here to set your password and log in: <%= setNewPasswordUrl %>
If you have any questions or problems, please contact #{settings.adminEmail}
Click here to set your password and log in:
"""
compiledTemplate: _.template """
<p>Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".</p>
secondaryMessage: () -> "If you have any questions or problems, please contact #{settings.adminEmail}"
ctaText: () -> "Set password"
ctaURL: (opts) -> opts.setNewPasswordUrl
})
<p><a href="<%= setNewPasswordUrl %>">Click here to set your password and log in.</a></p>
<p>If you have any questions or problems, please contact <a href="mailto:#{settings.adminEmail}">#{settings.adminEmail}</a>.</p>
templates.canceledSubscription = CTAEmailTemplate({
subject: () -> "#{settings.appName} thoughts"
message: () -> """
I'm sorry to see you cancelled your #{settings.appName} premium account. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?
"""
secondaryMessage: () -> "Thank you in advance!"
ctaText: () -> "Leave Feedback"
ctaURL: (opts) -> "https://sharelatex.typeform.com/to/f5lBiZ"
})
templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
plainTextTemplate: _.template """
Hi <%= first_name %>,
I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
https://sharelatex.typeform.com/to/f5lBiZ
Thank you in advance.
Henry
ShareLaTeX Co-founder
"""
compiledTemplate: _.template '''
<p>Hi <%= first_name %>,</p>
<p>I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via <a href="https://sharelatex.typeform.com/to/f5lBiZ">this survey</a>?</p>
<p>Thank you in advance.</p>
<p>
Henry <br>
ShareLaTeX Co-founder
</p>
'''
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Password Reset
We got a request to reset your #{settings.appName} password.
Click this link to reset your password: <%= setNewPasswordUrl %>
templates.passwordResetRequested = CTAEmailTemplate({
subject: () -> "Password Reset - #{settings.appName}"
title: () -> "Password Reset"
message: () -> "We got a request to reset your #{settings.appName} password."
secondaryMessage: () -> """
If you ignore this message, your password won't be changed.
If you didn't request a password reset, let us know.
Thank you
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "Password Reset"
greeting: "Hi,"
message: "We got a request to reset your #{settings.appName} password."
secondaryMessage: "If you ignore this message, your password won't be changed.<br>If you didn't request a password reset, let us know."
ctaText: "Reset password"
ctaURL: opts.setNewPasswordUrl
gmailGoToAction: null
})
ctaText: () -> "Reset password"
ctaURL: (opts) -> opts.setNewPasswordUrl
})
templates.confirmEmail = CTAEmailTemplate({
subject: () -> "Confirm Email - #{settings.appName}"
title: () -> "Confirm Email"
message: () -> "Please confirm your email on #{settings.appName}."
ctaText: () -> "Confirm Email"
ctaURL: (opts) -> opts.confirmEmailUrl
})
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi, <%= owner.email %> wants to share '<%= project.name %>' with you.
templates.projectInvite = CTAEmailTemplate({
subject: (opts) -> "#{opts.project.name} - shared by #{opts.owner.email}"
title: (opts) -> "#{ opts.project.name } - shared by #{ opts.owner.email }"
message: (opts) -> "#{ opts.owner.email } wants to share '#{ opts.project.name }' with you."
ctaText: () -> "View project"
ctaURL: (opts) -> opts.inviteUrl
gmailGoToAction: (opts) ->
target: opts.inviteUrl
name: "View project"
description: "Join #{ opts.project.name } at #{ settings.appName }"
})
Follow this link to view the project: <%= inviteUrl %>
Thank you
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "#{ opts.project.name } &ndash; shared by #{ opts.owner.email }"
greeting: "Hi,"
message: "#{ opts.owner.email } wants to share &ldquo;#{ opts.project.name }&rdquo; with you."
secondaryMessage: null
ctaText: "View project"
ctaURL: opts.inviteUrl
gmailGoToAction:
target: opts.inviteUrl
name: "View project"
description: "Join #{ opts.project.name } at ShareLaTeX"
})
templates.verifyEmailToJoinTeam =
subject: _.template "<%= inviterName %> has invited you to join a team on #{settings.appName}"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Please click the button below to join the team and enjoy the benefits of an upgraded <%= appName %> account.
<%= acceptInviteUrl %>
Thank You
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "#{opts.inviterName} has invited you to join a team on #{settings.appName}"
greeting: "Hi,"
message: "Please click the button below to join the team and enjoy the benefits of an upgraded #{ opts.appName } account."
secondaryMessage: null
ctaText: "Verify now"
ctaURL: opts.acceptInviteUrl
gmailGoToAction: null
})
templates.testEmail =
subject: _.template "A Test Email from ShareLaTeX"
layout: BaseWithHeaderEmailLayout
type:"notification"
plainTextTemplate: _.template """
Hi,
This is a test email sent from ShareLaTeX.
#{settings.appName} - <%= siteUrl %>
"""
compiledTemplate: (opts) ->
SingleCTAEmailBody({
title: "A Test Email from ShareLaTeX"
greeting: "Hi,"
message: "This is a test email sent from ShareLaTeX"
secondaryMessage: null
ctaText: "Open ShareLaTeX"
ctaURL: "/"
gmailGoToAction: null
})
templates.verifyEmailToJoinTeam = CTAEmailTemplate({
subject: (opts) -> "#{ opts.inviterName } has invited you to join a team on #{settings.appName}"
title: (opts) -> "#{opts.inviterName} has invited you to join a team on #{settings.appName}"
message: (opts) -> "Please click the button below to join the team and enjoy the benefits of an upgraded #{ settings.appName } account."
ctaText: (opts) -> "Join now"
ctaURL: (opts) -> opts.acceptInviteUrl
})
templates.testEmail = CTAEmailTemplate({
subject: () -> "A Test Email from #{settings.appName}"
title: () -> "A Test Email from #{settings.appName}"
greeting: () -> "Hi,"
message: () -> "This is a test Email from #{settings.appName}"
ctaText: () -> "Open #{settings.appName}"
ctaURL: () -> settings.siteUrl
})
module.exports =
templates: templates
CTAEmailTemplate: CTAEmailTemplate
buildEmail: (templateName, opts)->
template = templates[templateName]
opts.siteUrl = settings.siteUrl
@@ -180,5 +125,4 @@ module.exports =
subject : template.subject(opts)
html: template.layout(opts)
text: template?.plainTextTemplate?(opts)
type:template.type
}
@@ -61,6 +61,13 @@ ProjectHistoryDisabledError = (message) ->
return error
ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype
V1ConnectionError = (message) ->
error = new Error(message)
error.name = "V1ConnectionError"
error.__proto__ = V1ConnectionError.prototype
return error
V1ConnectionError.prototype.__proto___ = Error.prototype
module.exports = Errors =
NotFoundError: NotFoundError
ServiceNotConfiguredError: ServiceNotConfiguredError
@@ -71,3 +78,4 @@ module.exports = Errors =
UnsupportedExportRecordsError: UnsupportedExportRecordsError
V1HistoryNotSyncedError: V1HistoryNotSyncedError
ProjectHistoryDisabledError: ProjectHistoryDisabledError
V1ConnectionError: V1ConnectionError
@@ -4,11 +4,27 @@ ProjectLocator = require '../Project/ProjectLocator'
Settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
_ = require 'underscore'
LinkedFilesHandler = require './LinkedFilesHandler'
{
UrlFetchFailedError,
InvalidUrlError,
OutputFileFetchFailedError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
} = require './LinkedFilesErrors'
module.exports = LinkedFilesController = {
Agents: {
url: require('./UrlAgent'),
project_file: require('./ProjectFileAgent')
project_file: require('./ProjectFileAgent'),
project_output_file: require('./ProjectOutputFileAgent')
}
_getAgent: (provider) ->
@@ -18,15 +34,6 @@ module.exports = LinkedFilesController = {
return null
LinkedFilesController.Agents[provider]
_getFileById: (project_id, file_id, callback=(err, file)->) ->
ProjectLocator.findElement {
project_id,
element_id: file_id,
type: 'file'
}, (err, file, path, parentFolder) ->
return callback(err) if err?
callback(null, file, path, parentFolder)
createLinkedFile: (req, res, next) ->
{project_id} = req.params
{name, provider, data, parent_folder_id} = req.body
@@ -37,23 +44,23 @@ module.exports = LinkedFilesController = {
if !Agent?
return res.sendStatus(400)
linkedFileData = Agent.sanitizeData(data)
linkedFileData.provider = provider
data.provider = provider
if !Agent.canCreate(linkedFileData)
return res.status(403).send('Cannot create linked file')
LinkedFilesController._doImport(
req, res, next, Agent, project_id, user_id,
parent_folder_id, name, linkedFileData
)
Agent.createLinkedFile project_id,
data,
name,
parent_folder_id,
user_id,
(err, newFileId) ->
return LinkedFilesController.handleError(err, req, res, next) if err?
res.json(new_file_id: newFileId)
refreshLinkedFile: (req, res, next) ->
{project_id, file_id} = req.params
user_id = AuthenticationController.getLoggedInUserId(req)
logger.log {project_id, file_id, user_id}, 'refresh linked file request'
LinkedFilesController._getFileById project_id, file_id, (err, file, path, parentFolder) ->
LinkedFilesHandler.getFileById project_id, file_id, (err, file, path, parentFolder) ->
return next(err) if err?
return res.sendStatus(404) if !file?
name = file.name
@@ -65,37 +72,51 @@ module.exports = LinkedFilesController = {
Agent = LinkedFilesController._getAgent(provider)
if !Agent?
return res.sendStatus(400)
LinkedFilesController._doImport(
req, res, next, Agent, project_id, user_id,
parent_folder_id, name, linkedFileData
Agent.refreshLinkedFile project_id,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, newFileId) ->
return LinkedFilesController.handleError(err, req, res, next) if err?
res.json(new_file_id: newFileId)
handleError: (error, req, res, next) ->
if error instanceof BadDataError
res.status(400).send("The submitted data is not valid")
else 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 if error instanceof V1ProjectNotFoundError
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
else if error instanceof OutputFileFetchFailedError
res.status(404).send("Could not get output file")
else if error instanceof UrlFetchFailedError
res.status(422).send(
"Your URL could not be reached (#{error.statusCode} status code). Please check it and try again."
)
_doImport: (req, res, next, Agent, project_id, user_id, parent_folder_id, name, linkedFileData) ->
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
else if error instanceof InvalidUrlError
res.status(422).send(
"Your URL is not valid. Please check it and try again."
)
}
else
next(error)
}
@@ -0,0 +1,84 @@
UrlFetchFailedError = (message) ->
error = new Error(message)
error.name = 'UrlFetchFailedError'
error.__proto__ = UrlFetchFailedError.prototype
return error
UrlFetchFailedError.prototype.__proto__ = Error.prototype
InvalidUrlError = (message) ->
error = new Error(message)
error.name = 'InvalidUrlError'
error.__proto__ = InvalidUrlError.prototype
return error
InvalidUrlError.prototype.__proto__ = Error.prototype
OutputFileFetchFailedError = (message) ->
error = new Error(message)
error.name = 'OutputFileFetchFailedError'
error.__proto__ = OutputFileFetchFailedError.prototype
return error
OutputFileFetchFailedError.prototype.__proto__ = Error.prototype
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
V1ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'V1ProjectNotFound'
error.__proto__ = V1ProjectNotFoundError.prototype
return error
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
SourceFileNotFoundError = (message) ->
error = new Error(message)
error.name = 'SourceFileNotFound'
error.__proto__ = SourceFileNotFoundError.prototype
return error
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
module.exports = {
UrlFetchFailedError,
InvalidUrlError,
OutputFileFetchFailedError,
AccessDeniedError,
BadEntityTypeError,
BadDataError,
ProjectNotFoundError,
V1ProjectNotFoundError,
SourceFileNotFoundError,
}
@@ -0,0 +1,86 @@
FileWriter = require '../../infrastructure/FileWriter'
EditorController = require '../Editor/EditorController'
ProjectLocator = require '../Project/ProjectLocator'
Project = require("../../models/Project").Project
ProjectGetter = require("../Project/ProjectGetter")
_ = require 'underscore'
{
ProjectNotFoundError,
V1ProjectNotFoundError,
BadDataError
} = require './LinkedFilesErrors'
module.exports = LinkedFilesHandler =
getFileById: (project_id, file_id, callback=(err, file)->) ->
ProjectLocator.findElement {
project_id,
element_id: file_id,
type: 'file'
}, (err, file, path, parentFolder) ->
return callback(err) if err?
callback(null, file, path, parentFolder)
getSourceProject: (data, callback=(err, project)->) ->
projection = {_id: 1, name: 1}
if data.v1_source_doc_id?
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new V1ProjectNotFoundError())
callback(null, project)
else if data.source_project_id?
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new ProjectNotFoundError())
callback(null, project)
else
callback(new BadDataError('neither v1 nor v2 id present'))
importFromStream: (
project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
callback=(err, file)->
) ->
callback = _.once(callback)
FileWriter.writeStreamToDisk project_id, readStream, (err, fsPath) ->
return callback(err) if err?
EditorController.upsertFile project_id,
parent_folder_id,
name,
fsPath,
linkedFileData,
"upload",
user_id,
(err, file) =>
return callback(err) if err?
callback(null, file)
importContent: (
project_id,
content,
linkedFileData,
name,
parent_folder_id,
user_id,
callback=(err, file)->
) ->
callback = _.once(callback)
FileWriter.writeContentToDisk project_id, content, (err, fsPath) ->
return callback(err) if err?
EditorController.upsertFile project_id,
parent_folder_id,
name,
fsPath,
linkedFileData,
"upload",
user_id,
(err, file) =>
return callback(err) if err?
callback(null, file)
@@ -1,68 +1,94 @@
FileWriter = require('../../infrastructure/FileWriter')
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectLocator = require('../Project/ProjectLocator')
ProjectGetter = require('../Project/ProjectGetter')
Project = require("../../models/Project").Project
DocstoreManager = require('../Docstore/DocstoreManager')
FileStoreHandler = require('../FileStore/FileStoreHandler')
FileWriter = require('../../infrastructure/FileWriter')
_ = require "underscore"
Settings = require 'settings-sharelatex'
LinkedFilesHandler = require './LinkedFilesHandler'
{
BadDataError,
AccessDeniedError,
BadEntityTypeError,
SourceFileNotFoundError,
ProjectNotFoundError,
V1ProjectNotFoundError
} = require './LinkedFilesErrors'
module.exports = ProjectFileAgent = {
AccessDeniedError = (message) ->
error = new Error(message)
error.name = 'AccessDenied'
error.__proto__ = AccessDeniedError.prototype
return error
AccessDeniedError.prototype.__proto__ = Error.prototype
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
if !@_canCreate(linkedFileData)
return callback(new AccessDeniedError())
@_go(project_id, linkedFileData, name, parent_folder_id, user_id, callback)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@_go project_id, linkedFileData, name, parent_folder_id, user_id, callback
BadEntityTypeError = (message) ->
error = new Error(message)
error.name = 'BadEntityType'
error.__proto__ = BadEntityTypeError.prototype
return error
BadEntityTypeError.prototype.__proto__ = Error.prototype
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
return callback(err) if err?
return callback(new AccessDeniedError()) if !allowed
if !@_validate(linkedFileData)
return callback(new BadDataError())
callback(null, linkedFileData)
_go: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
linkedFileData = @_sanitizeData(linkedFileData)
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
if !@_validate(linkedFileData)
return callback(new BadDataError())
@_getEntity linkedFileData, user_id, (err, source_project, entity, type) =>
return callback(err) if err?
if type == 'doc'
DocstoreManager.getDoc source_project._id, entity._id, (err, lines) ->
return callback(err) if err?
LinkedFilesHandler.importContent project_id,
lines.join('\n'),
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else if type == 'file'
FileStoreHandler.getFileStream source_project._id, entity._id, null, (err, fileStream) ->
return callback(err) if err?
LinkedFilesHandler.importFromStream project_id,
fileStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
callback(new BadEntityTypeError())
BadDataError = (message) ->
error = new Error(message)
error.name = 'BadData'
error.__proto__ = BadDataError.prototype
return error
BadDataError.prototype.__proto__ = Error.prototype
_getEntity:
(linkedFileData, current_user_id, callback = (err, entity, type) ->) ->
callback = _.once(callback)
{ source_entity_path } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
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)
callback(null, project, entity, type)
ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'ProjectNotFound'
error.__proto__ = ProjectNotFoundError.prototype
return error
ProjectNotFoundError.prototype.__proto__ = Error.prototype
V1ProjectNotFoundError = (message) ->
error = new Error(message)
error.name = 'V1ProjectNotFound'
error.__proto__ = V1ProjectNotFoundError.prototype
return error
V1ProjectNotFoundError.prototype.__proto__ = Error.prototype
SourceFileNotFoundError = (message) ->
error = new Error(message)
error.name = 'SourceFileNotFound'
error.__proto__ = SourceFileNotFoundError.prototype
return error
SourceFileNotFoundError.prototype.__proto__ = Error.prototype
module.exports = ProjectFileAgent =
sanitizeData: (data) ->
_sanitizeData: (data) ->
return _.pick(
data,
'provider',
'source_project_id',
'v1_source_doc_id',
'source_entity_path'
@@ -74,34 +100,13 @@ module.exports = ProjectFileAgent =
data.source_entity_path?
)
canCreate: (data) ->
_canCreate: (data) ->
# Don't allow creation of linked-files with v1 doc ids
!data.v1_source_doc_id?
_getSourceProject: (data, callback=(err, project)->) ->
projection = {_id: 1, name: 1}
if data.v1_source_doc_id?
Project.findOne {'overleaf.id': data.v1_source_doc_id}, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new V1ProjectNotFoundError())
callback(null, project)
else if data.source_project_id?
ProjectGetter.getProject data.source_project_id, projection, (err, project) ->
return callback(err) if err?
if !project?
return callback(new ProjectNotFoundError())
callback(null, project)
else
callback(new BadDataError('neither v1 nor v2 id present'))
_getSourceProject: LinkedFilesHandler.getSourceProject
decorateLinkedFileData: (data, callback = (err, newData) ->) ->
callback = _.once(callback)
@_getSourceProject data, (err, project) ->
return callback(err) if err?
callback(err, _.extend(data, {source_project_display_name: project.name}))
checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
_checkAuth: (project_id, data, current_user_id, callback = (error, allowed)->) ->
callback = _.once(callback)
if !ProjectFileAgent._validate(data)
return callback(new BadDataError())
@@ -110,52 +115,4 @@ module.exports = ProjectFileAgent =
AuthorizationManager.canUserReadProject current_user_id, 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_entity_path } = data
@_getSourceProject data, (err, project) ->
return callback(err) if err?
source_project_id = project._id
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 if error instanceof V1ProjectNotFoundError
res.status(409).send("Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file")
else
next(error)
next()
}
@@ -0,0 +1,158 @@
AuthorizationManager = require('../Authorization/AuthorizationManager')
ProjectGetter = require('../Project/ProjectGetter')
Settings = require 'settings-sharelatex'
CompileManager = require '../Compile/CompileManager'
ClsiManager = require '../Compile/ClsiManager'
ProjectFileAgent = require './ProjectFileAgent'
_ = require "underscore"
{
BadDataError,
AccessDeniedError,
BadEntityTypeError,
OutputFileFetchFailedError
} = require './LinkedFilesErrors'
LinkedFilesHandler = require './LinkedFilesHandler'
logger = require 'logger-sharelatex'
module.exports = ProjectOutputFileAgent = {
_prepare: (project_id, linkedFileData, user_id, callback=(err, linkedFileData)->) ->
@_checkAuth project_id, linkedFileData, user_id, (err, allowed) =>
return callback(err) if err?
return callback(new AccessDeniedError()) if !allowed
if !@_validate(linkedFileData)
return callback(new BadDataError())
callback(null, linkedFileData)
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
if !@_canCreate(linkedFileData)
return callback(new AccessDeniedError())
linkedFileData = @_sanitizeData(linkedFileData)
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
@_getFileStream linkedFileData, user_id, (err, readStream) =>
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) =>
if 200 <= response.statusCode < 300
readStream.resume()
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
err = new OutputFileFetchFailedError(
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
)
err.statusCode = response.statusCode
callback(err)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@_prepare project_id, linkedFileData, user_id, (err, linkedFileData) =>
return callback(err) if err?
@_compileAndGetFileStream linkedFileData, user_id, (err, readStream, new_build_id) =>
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) =>
if 200 <= response.statusCode < 300
readStream.resume()
linkedFileData.build_id = new_build_id
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
err = new OutputFileFetchFailedError(
"Output file fetch failed: #{linkedFileData.build_id}, #{linkedFileData.source_output_file_path}"
)
err.statusCode = response.statusCode
callback(err)
_sanitizeData: (data) ->
return {
provider: data.provider,
source_project_id: data.source_project_id,
source_output_file_path: data.source_output_file_path,
build_id: data.build_id
}
_canCreate: ProjectFileAgent._canCreate
_getSourceProject: LinkedFilesHandler.getSourceProject
_validate: (data) ->
return (
(data.source_project_id? || data.v1_source_doc_id?) &&
data.source_output_file_path? &&
data.build_id?
)
_checkAuth: (project_id, data, current_user_id, callback = (err, allowed)->) ->
callback = _.once(callback)
if !@_validate(data)
return callback(new BadDataError())
@_getSourceProject data, (err, project) ->
return callback(err) if err?
AuthorizationManager.canUserReadProject current_user_id,
project._id,
null,
(err, canRead) ->
return callback(err) if err?
callback(null, canRead)
_getFileStream: (linkedFileData, user_id, callback=(err, fileStream)->) ->
callback = _.once(callback)
{ source_output_file_path, build_id } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
ClsiManager.getOutputFileStream source_project_id,
user_id,
build_id,
source_output_file_path,
(err, readStream) ->
return callback(err) if err?
readStream.pause()
callback(null, readStream)
_compileAndGetFileStream: (linkedFileData, user_id, callback=(err, stream, build_id)->) ->
callback = _.once(callback)
{ source_output_file_path } = linkedFileData
@_getSourceProject linkedFileData, (err, project) ->
return callback(err) if err?
source_project_id = project._id
CompileManager.compile source_project_id,
user_id,
{},
(err, status, outputFiles) ->
return callback(err) if err?
if status != 'success'
return callback(new OutputFileFetchFailedError())
outputFile = _.find(
outputFiles,
(o) => o.path == source_output_file_path
)
if !outputFile?
return callback(new OutputFileFetchFailedError())
build_id = outputFile.build
ClsiManager.getOutputFileStream source_project_id,
user_id,
build_id,
source_output_file_path,
(err, readStream) ->
return callback(err) if err?
readStream.pause()
callback(null, readStream, build_id)
}
@@ -1,67 +1,53 @@
request = require 'request'
FileWriter = require('../../infrastructure/FileWriter')
_ = require "underscore"
urlValidator = require 'valid-url'
Settings = require 'settings-sharelatex'
{ InvalidUrlError, UrlFetchFailedError } = require './LinkedFilesErrors'
LinkedFilesHandler = require './LinkedFilesHandler'
UrlFetchFailedError = (message) ->
error = new Error(message)
error.name = 'UrlFetchFailedError'
error.__proto__ = UrlFetchFailedError.prototype
return error
UrlFetchFailedError.prototype.__proto__ = Error.prototype
InvalidUrlError = (message) ->
error = new Error(message)
error.name = 'InvalidUrlError'
error.__proto__ = InvalidUrlError.prototype
return error
InvalidUrlError.prototype.__proto__ = Error.prototype
module.exports = UrlAgent = {
UrlFetchFailedError: UrlFetchFailedError
InvalidUrlError: InvalidUrlError
sanitizeData: (data) ->
createLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
linkedFileData = @._sanitizeData(linkedFileData)
@_getUrlStream project_id, linkedFileData, user_id, (err, readStream) ->
return callback(err) if err?
readStream.on "error", callback
readStream.on "response", (response) ->
if 200 <= response.statusCode < 300
readStream.resume()
LinkedFilesHandler.importFromStream project_id,
readStream,
linkedFileData,
name,
parent_folder_id,
user_id,
(err, file) ->
return callback(err) if err?
callback(null, file._id) # Created
else
error = new UrlFetchFailedError("url fetch failed: #{linkedFileData.url}")
error.statusCode = response.statusCode
callback(error)
refreshLinkedFile: (project_id, linkedFileData, name, parent_folder_id, user_id, callback) ->
@createLinkedFile project_id, linkedFileData, name, parent_folder_id, user_id, callback
_sanitizeData: (data) ->
return {
provider: data.provider
url: @._prependHttpIfNeeded(data.url)
}
canCreate: (data) -> true
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) ->) ->
_getUrlStream: (project_id, data, current_user_id, callback = (error, fsPath) ->) ->
callback = _.once(callback)
url = data.url
if !urlValidator.isWebUri(url)
return callback(new InvalidUrlError("invalid url: #{url}"))
url = UrlAgent._wrapWithProxy(url)
url = @_wrapWithProxy(url)
readStream = request.get(url)
readStream.on "error", callback
readStream.on "response", (response) ->
if 200 <= response.statusCode < 300
FileWriter.writeStreamToDisk project_id, readStream, callback
else
error = new UrlFetchFailedError("url fetch failed: #{url}")
error.statusCode = response.statusCode
callback(error)
handleError: (error, req, res, next) ->
if error instanceof UrlFetchFailedError
res.status(422).send(
"Your URL could not be reached (#{error.statusCode} status code). Please check it and try again."
)
else if error instanceof InvalidUrlError
res.status(422).send(
"Your URL is not valid. Please check it and try again."
)
else
next(error)
readStream.pause()
callback(null, readStream)
_prependHttpIfNeeded: (url) ->
if !url.match('://')
@@ -14,7 +14,7 @@ module.exports =
if !user? or user.holdingAccount
logger.err email:email, "user could not be found for password reset"
return callback(null, false)
OneTimeTokenHandler.getNewToken user._id, (err, token)->
OneTimeTokenHandler.getNewToken 'password', user._id, (err, token)->
if err then return callback(err)
emailOptions =
to : email
@@ -24,7 +24,7 @@ module.exports =
callback null, true
setNewUserPassword: (token, password, callback = (error, found, user_id) ->)->
OneTimeTokenHandler.getValueFromTokenAndExpire token, (err, user_id)->
OneTimeTokenHandler.getValueFromTokenAndExpire 'password', token, (err, user_id)->
if err then return callback(err)
if !user_id?
return callback null, false, null
@@ -27,6 +27,7 @@ CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
Modules = require '../../infrastructure/Modules'
ProjectEntityHandler = require './ProjectEntityHandler'
crypto = require 'crypto'
{ V1ConnectionError } = require '../Errors/Errors'
module.exports = ProjectController =
@@ -179,11 +180,14 @@ module.exports = ProjectController =
ProjectGetter.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref tokens', cb
v1Projects: (cb) ->
Modules.hooks.fire "findAllV1Projects", user_id, (error, projects = []) ->
if error? and error.message == 'No V1 connection'
if error? and error instanceof V1ConnectionError
return cb(null, projects: [], tags: [], noConnection: true)
return cb(error, projects[0]) # hooks.fire returns an array of results, only need first
hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, cb
LimitationsManager.userHasSubscriptionOrIsGroupMember currentUser, (error, hasSub) ->
if error? and error instanceof V1ConnectionError
return cb(null, true)
return cb(error, hasSub)
user: (cb) ->
User.findById user_id, "featureSwitches overleaf awareOfV2 features", cb
}, (err, results)->
@@ -210,7 +214,7 @@ module.exports = ProjectController =
tags: tags
notifications: notifications or []
user: user
hasSubscription: results.hasSubscription[0]
hasSubscription: results.hasSubscription
isShowingV1Projects: results.v1Projects?
warnings: warnings
}
@@ -55,7 +55,10 @@ module.exports = ProjectEntityUpdateHandler = self =
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)
fileProperties = name : SafePath.clean(origonalFileRef.name)
if origonalFileRef.linkedFileData?
fileProperties.linkedFileData = origonalFileRef.linkedFileData
fileRef = new File(fileProperties)
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"
@@ -1,34 +1,50 @@
Settings = require('settings-sharelatex')
RedisWrapper = require("../../infrastructure/RedisWrapper")
rclient = RedisWrapper.client("one_time_token")
crypto = require("crypto")
logger = require("logger-sharelatex")
{db} = require "../../infrastructure/mongojs"
Errors = require "../Errors/Errors"
ONE_HOUR_IN_S = 60 * 60
buildKey = (token)-> return "password_token:#{token}"
module.exports =
getNewToken: (value, options = {}, callback)->
getNewToken: (use, data, options = {}, callback = (error, data) ->)->
# options is optional
if typeof options == "function"
callback = options
options = {}
expiresIn = options.expiresIn or ONE_HOUR_IN_S
logger.log value:value, "generating token for password reset"
createdAt = new Date()
expiresAt = new Date(createdAt.getTime() + expiresIn * 1000)
token = crypto.randomBytes(32).toString("hex")
multi = rclient.multi()
multi.set buildKey(token), value
multi.expire buildKey(token), expiresIn
multi.exec (err)->
callback(err, token)
logger.log {data, expiresIn, token_start: token.slice(0,8)}, "generating token for #{use}"
db.tokens.insert {
use: use
token: token,
data: data,
createdAt: createdAt,
expiresAt: expiresAt
}, (error) ->
return callback(error) if error?
callback null, token
getValueFromTokenAndExpire: (token, callback)->
logger.log token:token, "getting user id from password token"
multi = rclient.multi()
multi.get buildKey(token)
multi.del buildKey(token)
multi.exec (err, results)->
callback err, results?[0]
getValueFromTokenAndExpire: (use, token, callback = (error, data) ->)->
logger.log token_start: token.slice(0,8), "getting data from #{use} token"
now = new Date()
db.tokens.findAndModify {
query: {
use: use,
token: token,
expiresAt: { $gt: now },
usedAt: { $exists: false }
},
update: {
$set: {
usedAt: now
}
}
}, (error, token) ->
return callback(error) if error?
if !token?
return callback(new Errors.NotFoundError('no token found'))
return callback null, token.data
@@ -56,6 +56,20 @@ module.exports = TeamInvitesHandler =
return callback(error) if error?
createInvite(subscription, email, inviterName, callback)
importInvite: (subscription, inviterName, email, token, sentAt, callback) ->
checkIfInviteIsPossible subscription, email, (error, possible, reason) ->
return callback(error) if error?
return callback(reason) unless possible
subscription.teamInvites.push({
email: email
inviterName: inviterName
token: token
sentAt: sentAt
})
subscription.save callback
acceptInvite: (token, userId, callback) ->
logger.log {userId}, "Accepting invite"
TeamInvitesHandler.getInvite token, (err, invite, subscription) ->
@@ -93,7 +107,6 @@ createInvite = (subscription, email, inviterName, callback) ->
return callback(error) if error?
return callback(reason) unless possible
invite = subscription.teamInvites.find (invite) -> invite.email == email
if !invite?
@@ -2,6 +2,7 @@ UserGetter = require "../User/UserGetter"
request = require "request"
settings = require "settings-sharelatex"
logger = require "logger-sharelatex"
{ V1ConnectionError } = require "../Errors/Errors"
module.exports = V1SubscriptionManager =
# Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null
@@ -58,7 +59,10 @@ module.exports = V1SubscriptionManager =
json: true,
timeout: 5 * 1000
}, (error, response, body) ->
return callback(error) if error?
if error?
# Specially handle no connection err, so warning can be shown
error = new V1ConnectionError('No V1 connection') if error.code == 'ECONNREFUSED'
return callback(error)
if 200 <= response.statusCode < 300
return callback null, body
else
@@ -0,0 +1,36 @@
EmailHelper = require "../Helpers/EmailHelper"
EmailHandler = require "../Email/EmailHandler"
OneTimeTokenHandler = require "../Security/OneTimeTokenHandler"
settings = require 'settings-sharelatex'
Errors = require "../Errors/Errors"
logger = require "logger-sharelatex"
UserUpdater = require "./UserUpdater"
ONE_YEAR_IN_S = 365 * 24 * 60 * 60
module.exports = UserEmailsConfirmationHandler =
sendConfirmationEmail: (user_id, email, emailTemplate, callback = (error) ->) ->
if arguments.length == 3
callback = emailTemplate
emailTemplate = 'confirmEmail'
email = EmailHelper.parseEmail(email)
return callback(new Error('invalid email')) if !email?
data = {user_id, email}
OneTimeTokenHandler.getNewToken 'email_confirmation', data, {expiresIn: ONE_YEAR_IN_S}, (err, token)->
return callback(err) if err?
emailOptions =
to: email
confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}"
EmailHandler.sendEmail emailTemplate, emailOptions, callback
confirmEmailFromToken: (token, callback = (error) ->) ->
logger.log {token_start: token.slice(0,8)}, 'confirming email from token'
OneTimeTokenHandler.getValueFromTokenAndExpire 'email_confirmation', token, (error, data) ->
return callback(error) if error?
if !data?
return callback(new Errors.NotFoundError('no token found'))
{user_id, email} = data
logger.log {data, user_id, email, token_start: token.slice(0,8)}, 'found data for email confirmation'
if !user_id? or email != EmailHelper.parseEmail(email)
return callback(new Errors.NotFoundError('invalid data'))
UserUpdater.confirmEmail user_id, email, callback
@@ -2,43 +2,72 @@ AuthenticationController = require('../Authentication/AuthenticationController')
UserGetter = require("./UserGetter")
UserUpdater = require("./UserUpdater")
EmailHelper = require("../Helpers/EmailHelper")
UserEmailsConfirmationHandler = require "./UserEmailsConfirmationHandler"
logger = require("logger-sharelatex")
Errors = require "../Errors/Errors"
module.exports = UserEmailsController =
list: (req, res) ->
list: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
UserGetter.getUserFullEmails userId, (error, fullEmails) ->
return res.sendStatus 500 if error?
return next(error) if error?
res.json fullEmails
add: (req, res) ->
add: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
email = EmailHelper.parseEmail(req.body.email)
return res.sendStatus 422 unless email?
UserUpdater.addEmailAddress userId, email, (error)->
return res.sendStatus 500 if error?
res.sendStatus 200
affiliationOptions =
university: req.body.university
role: req.body.role
department: req.body.department
UserUpdater.addEmailAddress userId, email, affiliationOptions, (error)->
return next(error) if error?
UserEmailsConfirmationHandler.sendConfirmationEmail userId, email, (err) ->
return next(error) if error?
res.sendStatus 204
remove: (req, res) ->
remove: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
logger.log req.params
email = EmailHelper.parseEmail(req.params.email)
return res.sendStatus 422 unless email?
UserUpdater.removeEmailAddress userId, email, (error)->
return res.sendStatus 500 if error?
return next(error) if error?
res.sendStatus 200
setDefault: (req, res) ->
setDefault: (req, res, next) ->
userId = AuthenticationController.getLoggedInUserId(req)
email = EmailHelper.parseEmail(req.body.email)
return res.sendStatus 422 unless email?
UserUpdater.setDefaultEmailAddress userId, email, (error)->
return res.sendStatus 500 if error?
return next(error) if error?
res.sendStatus 200
showConfirm: (req, res, next) ->
res.render 'user/confirm_email', {
token: req.query.token,
title: 'confirm_email'
}
confirm: (req, res, next) ->
token = req.body.token
if !token?
return res.sendStatus 422
UserEmailsConfirmationHandler.confirmEmailFromToken token, (error) ->
if error?
if error instanceof Errors.NotFoundError
res.status(404).json({
message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.'
})
else
next(error)
else
res.sendStatus 200
@@ -3,6 +3,8 @@ metrics = require('metrics-sharelatex')
logger = require('logger-sharelatex')
db = mongojs.db
ObjectId = mongojs.ObjectId
settings = require "settings-sharelatex"
request = require "request"
module.exports = UserGetter =
getUser: (query, projection, callback = (error, user) ->) ->
@@ -30,11 +32,9 @@ module.exports = UserGetter =
return callback error if error?
return callback new Error('User not Found') unless user
fullEmails = user.emails.map (emailData) ->
emailData.default = emailData.email == user.email
emailData
callback null, fullEmails
getAffiliations userId, (error, affiliationsData) ->
return callback error if error?
callback null, decorateFullEmails(user.email, user.emails, affiliationsData)
getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
email = email.trim()
@@ -81,6 +81,35 @@ module.exports = UserGetter =
return callback(message: 'alread_exists') if user?
callback(error)
decorateFullEmails = (defaultEmail, emailsData, affiliationsData) ->
emailsData.map (emailData) ->
emailData.default = emailData.email == defaultEmail
affiliation = affiliationsData.find (aff) -> aff.email == emailData.email
if affiliation?
{ institution, inferred, role, department } = affiliation
emailData.affiliation = { institution, inferred, role, department }
else
emailsData.affiliation = null
emailData
getAffiliations = (userId, callback = (error) ->) ->
return callback(null, []) unless settings?.apis?.v1?.url # service is not configured
request {
method: 'GET'
url: "#{settings.apis.v1.url}/api/v2/users/#{userId.toString()}/affiliations"
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
json: true,
timeout: 20 * 1000
}, (error, response, body) ->
return callback(error) if error?
unless 200 <= response.statusCode < 300
errorMessage = "Couldn't get affiliations: #{response.statusCode}"
return callback(new Error(errorMessage))
callback(null, body)
[
'getUser',
'getUserEmail',
@@ -74,7 +74,7 @@ module.exports = UserRegistrationHandler =
logger.log {email}, "user already exists, resending welcome email"
ONE_WEEK = 7 * 24 * 60 * 60 # seconds
OneTimeTokenHandler.getNewToken user._id, { expiresIn: ONE_WEEK }, (err, token)->
OneTimeTokenHandler.getNewToken 'password', user._id, { expiresIn: ONE_WEEK }, (err, token)->
return callback(err) if err?
setNewPasswordUrl = "#{settings.siteUrl}/user/activate?token=#{token}&user_id=#{user._id}"
@@ -5,6 +5,10 @@ db = mongojs.db
async = require("async")
ObjectId = mongojs.ObjectId
UserGetter = require("./UserGetter")
EmailHelper = require "../Helpers/EmailHelper"
Errors = require "../Errors/Errors"
settings = require "settings-sharelatex"
request = require "request"
module.exports = UserUpdater =
updateUser: (query, update, callback = (error) ->) ->
@@ -42,30 +46,43 @@ module.exports = UserUpdater =
# Add a new email address for the user. Email cannot be already used by this
# or any other user
addEmailAddress: (userId, newEmail, callback) ->
addEmailAddress: (userId, newEmail, affiliationOptions, callback) ->
unless callback? # affiliationOptions is optional
callback = affiliationOptions
affiliationOptions = {}
UserGetter.ensureUniqueEmailAddress newEmail, (error) =>
return callback(error) if error?
update = $push: emails: email: newEmail, createdAt: new Date()
@updateUser userId, update, (error) ->
addAffiliation userId, newEmail, affiliationOptions, (error) =>
if error?
logger.err error: error, 'problem updating users emails'
logger.err error: error, 'problem adding affiliation'
return callback(error)
callback()
update = $push: emails: email: newEmail, createdAt: new Date()
@updateUser userId, update, (error) ->
if error?
logger.err error: error, 'problem updating users emails'
return callback(error)
callback()
# remove one of the user's email addresses. The email cannot be the user's
# default email address
removeEmailAddress: (userId, email, callback) ->
query = _id: userId, email: $ne: email
update = $pull: emails: email: email
@updateUser query, update, (error, res) ->
removeAffiliation userId, email, (error) =>
if error?
logger.err error:error, 'problem removing users email'
logger.err error: error, 'problem removing affiliation'
return callback(error)
if res.nMatched == 0
return callback(new Error('Cannot remove default email'))
callback()
query = _id: userId, email: $ne: email
update = $pull: emails: email: email
@updateUser query, update, (error, res) ->
if error?
logger.err error:error, 'problem removing users email'
return callback(error)
if res.n == 0
return callback(new Error('Cannot remove email'))
callback()
# set the default email address by setting the `email` attribute. The email
@@ -77,10 +94,67 @@ module.exports = UserUpdater =
if error?
logger.err error:error, 'problem setting default emails'
return callback(error)
if res.nMatched == 0
if res.n == 0 # TODO: Check n or nMatched?
return callback(new Error('Default email does not belong to user'))
callback()
confirmEmail: (userId, email, callback) ->
email = EmailHelper.parseEmail(email)
return callback(new Error('invalid email')) if !email?
logger.log {userId, email}, 'confirming user email'
query =
_id: userId
'emails.email': email
update =
$set:
'emails.$.confirmedAt': new Date()
@updateUser query, update, (error, res) ->
return callback(error) if error?
logger.log {res, userId, email}, "tried to confirm email"
if res.n == 0
return callback(new Errors.NotFoundError('user id and email do no match'))
callback()
addAffiliation = (userId, email, { university, department, role }, callback = (error) ->) ->
makeAffiliationRequest {
method: 'POST'
path: "/api/v2/users/#{userId.toString()}/affiliations"
body: { email, university, department, role }
defaultErrorMessage: "Couldn't create affiliation"
}, callback
removeAffiliation = (userId, email, callback = (error) ->) ->
email = encodeURIComponent(email)
makeAffiliationRequest {
method: 'DELETE'
path: "/api/v2/users/#{userId.toString()}/affiliations/#{email}"
extraSuccessStatusCodes: [404] # `Not Found` responses are considered successful
defaultErrorMessage: "Couldn't remove affiliation"
}, callback
makeAffiliationRequest = (requestOptions, callback = (error) ->) ->
return callback(null) unless settings?.apis?.v1?.url # service is not configured
requestOptions.extraSuccessStatusCodes ||= []
request {
method: requestOptions.method
url: "#{settings.apis.v1.url}#{requestOptions.path}"
body: requestOptions.body
auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
json: true,
timeout: 20 * 1000
}, (error, response, body) ->
return callback(error) if error?
isSuccess = 200 <= response.statusCode < 300
isSuccess ||= response.statusCode in requestOptions.extraSuccessStatusCodes
unless isSuccess
if body?.errors
errorMessage = "#{response.statusCode}: #{body.errors}"
else
errorMessage = "#{requestOptions.defaultErrorMessage}: #{response.statusCode}"
return callback(new Error(errorMessage))
callback(null)
[
'updateUser'
'changeEmailAddress'
@@ -15,11 +15,14 @@ module.exports = FileWriter =
callback(null)
writeLinesToDisk: (identifier, lines, callback = (error, fsPath)->) ->
FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback)
writeContentToDisk: (identifier, content, 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) ->
fs.writeFile fsPath, content, (error) ->
return callback(error) if error?
callback(null, fsPath)
@@ -1,16 +0,0 @@
settings = require("settings-sharelatex")
logger = require("logger-sharelatex")
request = require("request")
module.exports = (req, res, next)->
requestedUrl = req.url
redirectUrl = settings.proxyUrls[requestedUrl]
if redirectUrl?
logger.log redirectUrl:redirectUrl, reqUrl:req.url, "proxying url"
upstream = request(redirectUrl)
upstream.on "error", (error) ->
logger.error err: error, "error in OldAssetProxy"
upstream.pipe(res)
else
next()
@@ -0,0 +1,36 @@
settings = require 'settings-sharelatex'
logger = require 'logger-sharelatex'
request = require 'request'
URL = require 'url'
module.exports = ProxyManager =
call: (req, res, next) ->
requestUrl = URL.parse(req.url)
requestPath = requestUrl.pathname # ignore the query part
target = settings.proxyUrls[requestPath]
return next() unless target? # nothing to proxy
targetUrl = makeTargetUrl(target, req.query)
logger.log targetUrl: targetUrl, reqUrl: req.url, "proxying url"
upstream = request(targetUrl)
upstream.on "error", (error) ->
logger.error err: error, "error in ProxyManager"
# TODO: better handling of status code
# see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js
upstream.pipe(res)
# make a URL from a proxy target.
# if the query is specified, set/replace the target's query with the given query
makeTargetUrl = (target, query) ->
targetUrl = URL.parse(parseSettingUrl(target))
if query? and Object.keys(query).length > 0
targetUrl.query = query
targetUrl.search = null # clear `search` as it takes precedence over `query`
targetUrl.format()
parseSettingUrl = (target) ->
return target if typeof target is 'string'
"#{target.baseUrl}#{target.path or ''}"
@@ -32,7 +32,7 @@ Mongoose = require("./Mongoose")
oneDayInMilliseconds = 86400000
ReferalConnect = require('../Features/Referal/ReferalConnect')
RedirectManager = require("./RedirectManager")
OldAssetProxy = require("./OldAssetProxy")
ProxyManager = require("./ProxyManager")
translations = require("translations-sharelatex").setup(Settings.i18n)
Modules = require "./Modules"
@@ -74,7 +74,7 @@ app.use methodOverride()
app.use metrics.http.monitor(logger)
app.use RedirectManager
app.use OldAssetProxy
app.use ProxyManager.call
webRouter.use cookieParser(Settings.security.sessionSecret)
@@ -1,6 +1,6 @@
Settings = require "settings-sharelatex"
mongojs = require "mongojs"
db = mongojs(Settings.mongo.url, ["projects", "users", "userstubs"])
db = mongojs(Settings.mongo.url, ["projects", "users", "userstubs", "tokens"])
module.exports =
db: db
ObjectId: mongojs.ObjectId
+2 -1
View File
@@ -10,7 +10,8 @@ UserSchema = new Schema
email : {type : String, default : ''}
emails: [{
email: { type : String, default : '' },
createdAt: { type : Date, default: () -> new Date() }
createdAt: { type : Date, default: () -> new Date() },
confirmedAt: { type: Date }
}],
first_name : {type : String, default : ''}
last_name : {type : String, default : ''}
+20
View File
@@ -120,6 +120,10 @@ module.exports = class Router
webRouter.post '/user/emails/default',
AuthenticationController.requireLogin(),
UserEmailsController.setDefault
webRouter.get '/user/emails/confirm',
UserEmailsController.showConfirm
webRouter.post '/user/emails/confirm',
UserEmailsController.confirm
webRouter.get '/user/sessions',
AuthenticationController.requireLogin(),
@@ -295,6 +299,22 @@ module.exports = class Router
webRouter.post "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.submitPassword
# New "api" endpoints. Started as a way for v1 to call over to v2 (for
# long-term features, as opposed to the nominally temporary ones in the
# overleaf-integration module), but may expand beyond that role.
publicApiRouter.post '/api/clsi/compile/:submission_id', AuthenticationController.httpAuth, CompileController.compileSubmission
publicApiRouter.get /^\/api\/clsi\/compile\/([^\/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/,
((req, res, next) ->
params =
"submission_id": req.params[0]
"build_id": req.params[1]
"file": req.params[2]
req.params = params
next()
),
AuthenticationController.httpAuth,
CompileController.getFileFromClsiWithoutUser
#Admin Stuff
webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index
webRouter.get '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon
@@ -38,6 +38,7 @@ div.binary-file.full-size(
) #{translate("no_preview_available")}
div.binary-file-footer
// Linked Files: URL
div(ng-if="openFile.linkedFileData.provider == 'url'")
p
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
@@ -47,6 +48,7 @@ div.binary-file.full-size(
|
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
// Linked Files: Project File
div(ng-if="openFile.linkedFileData.provider == 'project_file'")
p
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
@@ -54,14 +56,30 @@ div.binary-file.full-size(
|
a(ng-if='!openFile.linkedFileData.v1_source_doc_id'
ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
| {{ openFile.linkedFileData.source_project_display_name }}
| Another project
span(ng-if='openFile.linkedFileData.v1_source_doc_id')
| {{ openFile.linkedFileData.source_project_display_name }}
| Another project
| /{{ 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'")
// Linked Files: Project Output File
div(ng-if="openFile.linkedFileData.provider == 'project_output_file'")
p
i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon
| Imported from the output of
|
a(ng-if='!openFile.linkedFileData.v1_source_doc_id'
ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank")
| Another project
span(ng-if='openFile.linkedFileData.v1_source_doc_id')
| Another project
| : {{ openFile.linkedFileData.source_output_file_path }},
|
| at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }}
// Bottom Controls
span(ng-if="openFile.linkedFileData.provider")
button.btn.btn-success(
href, ng-click="refreshFile(openFile)",
ng-disabled="refreshing"
@@ -308,165 +308,10 @@ script(type='text/ng-template', id='entityListItemTemplate')
ng-repeat="child in entity.children | orderBy:[orderByFoldersFirst, 'name']"
)
script(type='text/ng-template', id='newDocModalTemplate')
.modal-header
h3 #{translate("new_file")}
.modal-body
form(novalidate, name="newDocForm")
div.alert.alert-danger(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
input.form-control(
type="text",
placeholder="File Name",
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open",
valid-file,
name="name"
)
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="newDocForm.$invalid || state.inflight"
ng-click="create()"
)
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
// Project Linked Files Modal
script(type='text/ng-template', id='projectLinkedFileModalTemplate')
.modal-header
h3 New file from Project
.modal-body
div
div.alert.alert-danger(ng-if="state.error") Error, something went wrong!
div
form
.form-controls
label(for="project-select") Select a Project
span(ng-show="state.inFlight.projects")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-select"
ng-model="data.selectedProjectId"
ng-disabled="!shouldEnableProjectSelect()"
)
option(value="" disabled selected) - Please Select a Project
option(
ng-repeat="project in data.projects"
value="{{ project._id }}"
) {{ project.name }}
br
.form-controls
label(for="project-entity-select") Select a File
span(ng-show="state.inFlight.entities")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-entity-select"
ng-model="data.selectedProjectEntity"
ng-disabled="!shouldEnableProjectEntitySelect()"
)
option(value="" disabled selected) - Please Select a File
option(
ng-repeat="projectEntity in data.projectEntities"
value="{{ projectEntity.path }}"
) {{ projectEntity.path.slice(1) }}
br
.form-controls
label(for="name") File Name In This Project
input.form-control(
type="text"
placeholder="example.tex"
required
ng-model="data.name"
name="name"
)
br
.modal-footer
span(ng-show="state.inFlight.create")
i.fa.fa-spinner.fa-spin
| &nbsp;
button.btn.btn-default(
ng-disabled="state.inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="!shouldEnableCreateButton()"
ng-click="create()"
)
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='linkedFileModalTemplate')
.modal-header
h3 New file from URL
.modal-body
form(novalidate, name="newLinkedFileForm")
div.alert.alert-danger(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
label(for="url") URL to fetch the file from
input.form-control(
type="text",
placeholder="www.example.com/my_file",
required,
ng-model="inputs.url",
focus-on="open",
on-enter="create()",
name="url"
)
.row-spaced
label(for="name") File name in this project
input.form-control(
type="text",
placeholder="my_file",
required,
ng-model="inputs.name",
ng-change="nameChangedByUser = true"
valid-file,
on-enter="create()",
name="name"
)
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="newLinkedFileForm.$invalid || state.inflight"
ng-click="create()"
)
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type='text/ng-template', id='newFolderModalTemplate')
.modal-header
h3 #{translate("new_folder")}
.modal-body
div.alert.alert-danger(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
form(novalidate, name="newFolderForm")
input.form-control(
type="text",
@@ -478,8 +323,12 @@ script(type='text/ng-template', id='newFolderModalTemplate')
valid-file,
name="name"
)
.text-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
div.alert.alert-danger.row-spaced-small(ng-show="newFolderForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
div.alert.alert-danger.row-spaced-small(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
@@ -492,70 +341,7 @@ script(type='text/ng-template', id='newFolderModalTemplate')
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type="text/template", id="qq-file-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div.qq-upload-button-selector.btn.btn-primary.btn-lg
div #{translate('upload')}
span.or.btn-lg #{translate('or')}
span.drag-here.btn-lg #{translate('drag_here')}
span.qq-drop-processing-selector
span #{translate('processing')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
script(type="text/ng-template", id="uploadFileModalTemplate")
.modal-header
h3 #{translate("upload_files")}
.alert.alert-warning.small.modal-alert(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
.alert.alert-warning.small.modal-alert(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
.alert.alert-warning.small.modal-alert(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
.alert.alert-warning.small.modal-alert(ng-if="conflicts.length > 0")
p.text-center
| The following files already exist in this project:
ul.text-center.list-unstyled.row-spaced-small
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
p.text-center.row-spaced-small
| Do you want to overwrite them?
p.text-center
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
| &nbsp;
a(href, ng-click="cancel()").btn.btn-default Cancel
.modal-body(
fine-upload
endpoint="/project/{{ project_id }}/upload"
template-id="qq-file-uploader-template"
multiple="true"
auto-upload="false"
on-complete-callback="onComplete"
on-upload-callback="onUpload"
on-validate-batch="onValidateBatch"
on-error-callback="onError"
on-submit-callback="onSubmit"
on-cancel-callback="onCancel"
control="control"
params="{'folder_id': parent_folder_id}"
)
.modal-footer
button.btn.btn-default(ng-click="cancel()") #{translate("cancel")}
include ./new-file-modal
script(type='text/ng-template', id='deleteEntityModalTemplate')
.modal-header
@@ -69,14 +69,6 @@ aside#left-menu.full-size(
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")}
@@ -0,0 +1,217 @@
script(type='text/ng-template', id='newFileModalTemplate')
.modal-header
h3 Add Files
.modal-body.modal-new-file
table
tr
td.modal-new-file--list
ul.list-unstyled
li(ng-class="type == 'doc' ? 'active' : null")
a(href, ng-click="type = 'doc'")
i.fa.fa-fw.fa-file
|
| New File
li(ng-class="type == 'upload' ? 'active' : null")
a(href, ng-click="type = 'upload'")
i.fa.fa-fw.fa-upload
|
| Upload
li(ng-class="type == 'project' ? 'active' : null")
a(href, ng-click="type = 'project'")
i.fa.fa-fw.fa-folder-open
|
| From Another Project
li(ng-class="type == 'url' ? 'active' : null")
a(href, ng-click="type = 'url'")
i.fa.fa-fw.fa-globe
|
| From External URL
td(class="modal-new-file--body modal-new-file--body-{{type}}")
div(ng-if="type == 'doc'", ng-controller="NewDocModalController")
form(novalidate, name="newDocForm")
label(for="name") File Name
input.form-control(
type="text",
placeholder="File Name",
required,
ng-model="inputs.name",
on-enter="create()",
select-name-on="open",
valid-file,
name="name"
)
div.alert.alert-danger.row-spaced-small(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
div.alert.alert-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
div(ng-if="type == 'upload'", ng-controller="UploadFileModalController")
.alert.alert-warning.small.modal(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})}
.alert.alert-warning.small.modal(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")}
.alert.alert-warning.small.modal(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})}
.alert.alert-warning.small.modal(ng-if="conflicts.length > 0")
p.text-center
| The following files already exist in this project:
ul.text-center.list-unstyled.row-spaced-small
li(ng-repeat="conflict in conflicts"): strong {{ conflict }}
p.text-center.row-spaced-small
| Do you want to overwrite them?
p.text-center
a(href, ng-click="doUpload()").btn.btn-primary Overwrite
| &nbsp;
a(href, ng-click="cancel()").btn.btn-default Cancel
div(
fine-upload
endpoint="/project/{{ project_id }}/upload"
template-id="qq-file-uploader-template"
multiple="true"
auto-upload="false"
on-complete-callback="onComplete"
on-upload-callback="onUpload"
on-validate-batch="onValidateBatch"
on-error-callback="onError"
on-submit-callback="onSubmit"
on-cancel-callback="onCancel"
control="control"
params="{'folder_id': parent_folder_id}"
)
div(ng-if="type == 'project'", ng-controller="ProjectLinkedFileModalController")
div
form
.form-controls
label(for="project-select") Select a Project
span(ng-show="state.inFlight.projects")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-select"
ng-model="data.selectedProjectId"
ng-disabled="!shouldEnableProjectSelect()"
)
option(value="" disabled selected) - Please Select a Project
option(
ng-repeat="project in data.projects"
value="{{ project._id }}"
) {{ project.name }}
.form-controls.row-spaced-small(ng-if="!state.isOutputFilesMode")
label(for="project-entity-select") Select a File
span(ng-show="state.inFlight.entities")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-entity-select"
ng-model="data.selectedProjectEntity"
ng-disabled="!shouldEnableProjectEntitySelect()"
)
option(value="" disabled selected) - Please Select a File
option(
ng-repeat="projectEntity in data.projectEntities"
value="{{ projectEntity.path }}"
) {{ projectEntity.path.slice(1) }}
.form-controls.row-spaced-small(ng-if="state.isOutputFilesMode")
label(for="project-entity-select") Select an Output File
span(ng-show="state.inFlight.compile")
| &nbsp;
i.fa.fa-spinner.fa-spin
select.form-control(
name="project-output-file-select"
ng-model="data.selectedProjectOutputFile"
ng-disabled="!shouldEnableProjectOutputFileSelect()"
)
option(value="" disabled selected) - Please Select an Output File
option(
ng-repeat="outputFile in data.projectOutputFiles"
value="{{ outputFile.path }}"
) {{ outputFile.path }}
div.toggle-output-files-button
| or&nbsp;
a(
href="#"
ng-click="toggleOutputFilesMode()"
)
span(ng-show="state.isOutputFilesMode") select from source files
span(ng-show="!state.isOutputFilesMode") select from output files
.form-controls.row-spaced-small
label(for="name") File Name In This Project
input.form-control(
type="text"
placeholder="example.tex"
required
ng-model="data.name"
name="name"
)
div.alert.alert-danger.row-spaced-small(ng-if="state.error") Error, something went wrong!
div(ng-if="type == 'url'", ng-controller="UrlLinkedFileModalController")
form(novalidate, name="newLinkedFileForm")
label(for="url") URL to fetch the file from
input.form-control(
type="text",
placeholder="www.example.com/my_file",
required,
ng-model="inputs.url",
focus-on="open",
on-enter="create()",
name="url"
)
.row-spaced.small
label(for="name") File name in this project
input.form-control(
type="text",
placeholder="my_file",
required,
ng-model="inputs.name",
ng-change="nameChangedByUser = true"
valid-file,
on-enter="create()",
name="name"
)
.text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile")
| #{translate('files_cannot_include_invalid_characters')}
div.alert.alert-danger.row-spaced-small(ng-if="error")
div(ng-switch="error")
span(ng-switch-when="already exists") #{translate("file_already_exists")}
span(ng-switch-default) {{error}}
.modal-footer
button.btn.btn-default(
ng-disabled="state.inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="state.inflight || !state.valid"
ng-click="create()"
ng-hide="type == 'upload'"
)
span(ng-hide="state.inflight") #{translate("create")}
span(ng-show="state.inflight") #{translate("creating")}...
script(type="text/template", id="qq-file-uploader-template")
div.qq-uploader-selector
div(qq-hide-dropzone="").qq-upload-drop-area-selector.qq-upload-drop-area
span.qq-upload-drop-area-text-selector #{translate('drop_files_here_to_upload')}
div Drag here
div.row-spaced-small.small #{translate('or')}
div.row-spaced-small
div.qq-upload-button-selector.btn.btn-primary
| Select from your computer
span.qq-drop-processing-selector
span #{translate('processing')}
span.qq-drop-processing-spinner-selector
ul.qq-upload-list-selector
li
div.qq-progress-bar-container-selector
div(
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
class="qq-progress-bar-selector qq-progress-bar"
)
span.qq-upload-file-selector.qq-upload-file
span.qq-upload-size-selector.qq-upload-size
a(type="button").qq-btn.qq-upload-cancel-selector.qq-upload-cancel #{translate('cancel')}
button(type="button").qq-btn.qq-upload-retry-selector.qq-upload-retry #{translate('retry')}
span(role="status").qq-upload-status-text-selector.qq-upload-status-text
@@ -87,7 +87,7 @@ div
+plan_switch('table')
.col-md-3.text-right
+currency_dropdown
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true")
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label=`exp-{{plansVariant}}`)
.col-sm-12(ng-if="ui.view != 'student'")
+table_premium
.col-sm-12(ng-if="ui.view == 'student'")
+11 -1
View File
@@ -10,6 +10,7 @@ block scripts
block content
.content.content-alt
.container(ng-controller="NewSubscriptionController" ng-cloak)
.row.card-group
.col-md-5.col-md-push-4
@@ -35,6 +36,15 @@ block content
.row(ng-if="plansVariant == 'more-details' && planCode == 'student-annual' || plansVariant == 'more-details' && planCode == 'student-monthly'")
.col-xs-12
p.student-disclaimer #{translate('student_disclaimer')}
if plan_code == 'collaborator_free_trial_7_days' && !!settings.overleaf
.row
.col-xs-12
p.small.row-spaced-small
| The <strong>Collaborator</strong> plan is a new Overleaf v2 plan which includes track-changes, references search, and Dropbox &amp; GitHub integration. While Overleaf v2 is in beta, you can also still subscribe to a
|
a(href=settings.overleaf.host + "/plans") legacy plan in Overleaf v1
| .
hr.thin
.row
.col-md-12.text-center
@@ -239,7 +249,7 @@ block content
hr
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
p.small.text-center We're confident that you'll love #{settings.appName}, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
hr
span &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
@@ -18,17 +18,11 @@ block content
p.letter-from-founders
p #{translate("thanks_for_subscribing_you_help_sl", {planName:subscription.name})}
p #{translate("need_anything_contact_us_at")}
a(href='mailto:support@sharelatex.com') support@sharelatex.com
| . #{translate("goes_straight_to_our_inboxes")}.
a(href='mailto:support@sharelatex.com') #{settings.adminEmail}
| .
p #{translate("regards")},
br
| Henry and James
.portraits
span.img-circle
img(src=buildImgPath("about/henry_oswald.jpg"))
| &nbsp;
span.img-circle
img(src=buildImgPath("about/james_allen.jpg"))
| The #{settings.appName} Team
p
a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}
@@ -0,0 +1,28 @@
extends ../layout
block content
.content.content-alt
.container
.row
.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
.card
.page-header
h1 #{translate("confirm_email")}
form(
async-form="confirm-email",
name="confirmEmailForm"
action="/user/emails/confirm",
method="POST",
id="confirmEmailForm",
auto-submit="true",
ng-cloak
)
input(type="hidden", name="_csrf", value=csrfToken)
input(type="hidden", name="token", value=token)
form-messages(for="confirmEmailForm")
.alert.alert-success(ng-show="confirmEmailForm.response.success")
| Thank you, your email is now confirmed
p.text-center(ng-show="!confirmEmailForm.response.success && !confirmEmailForm.response.error")
i.fa.fa-fw.fa-spin.fa-spinner
|
| Confirming your email...
+1 -1
View File
@@ -17,7 +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'
ENABLED_LINKED_FILE_TYPES: 'url,project_file,project_output_file'
SHARELATEX_CONFIG: /app/test/acceptance/config/settings.test.coffee
depends_on:
- redis
@@ -15,11 +15,6 @@ define [
scope[attrs.name].response = response = {}
scope[attrs.name].inflight = false
element.on "submit", (e) ->
e.preventDefault()
validateCaptchaIfEnabled (response) ->
submitRequest response
validateCaptchaIfEnabled = (callback = (response) ->) ->
if attrs.captcha?
validateCaptcha callback
@@ -84,6 +79,17 @@ define [
text: data.message?.text or data.message or "Something went wrong talking to the server :(. Please try again."
type: 'error'
ga('send', 'event', formName, 'failure', data.message)
submit = () ->
validateCaptchaIfEnabled (response) ->
submitRequest response
element.on "submit", (e) ->
e.preventDefault()
submit()
if attrs.autoSubmit
submit()
}
App.directive "formMessages", () ->
@@ -44,13 +44,11 @@ define [
GraphicsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
{commandFragment, closingBrace} = context
{commandFragment} = Helpers.getContext(editor, pos)
if commandFragment
match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/)
if match
commandName = match[1]
currentArg = match[3]
[_, commandName, _, currentArg] = match
graphicsPaths = Preamble.getGraphicsPaths()
result = []
for graphic in Graphics.getGraphicsFiles()
@@ -60,8 +58,8 @@ define [
path = path.slice(graphicsPath.length)
break
result.push {
caption: "\\#{commandName}{#{path}#{closingBrace}"
value: "\\#{commandName}{#{path}#{closingBrace}"
caption: "\\#{commandName}{#{path}}"
value: "\\#{commandName}{#{path}}"
meta: "graphic"
score: 50
}
@@ -70,20 +68,18 @@ define [
metadataManager = @metadataManager
FilesCompleter =
getCompletions: (editor, session, pos, prefix, callback) =>
context = Helpers.getContext(editor, pos)
{commandFragment, closingBrace} = context
{commandFragment} = Helpers.getContext(editor, pos)
if commandFragment
match = commandFragment.match(/^\\(input|include){(\w*)/)
if match
commandName = match[1]
currentArg = match[2]
[_, commandName, currentArg] = match
result = []
for file in Files.getTeXFiles()
if file.id != @$scope.docId
path = file.path
result.push {
caption: "\\#{commandName}{#{path}#{closingBrace}"
value: "\\#{commandName}{#{path}#{closingBrace}"
caption: "\\#{commandName}{#{path}}"
value: "\\#{commandName}{#{path}}"
meta: "file"
score: 50
}
@@ -91,13 +87,11 @@ define [
LabelsCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
{commandFragment, closingBrace} = context
{commandFragment} = Helpers.getContext(editor, pos)
if commandFragment
refMatch = commandFragment.match(/^~?\\([a-z]*ref){([^}]*, *)?(\w*)/)
if refMatch
commandName = refMatch[1]
currentArg = refMatch[2]
[_, commandName, currentArg] = refMatch
result = []
if commandName != 'ref' # ref is in top 100 commands
result.push {
@@ -108,8 +102,8 @@ define [
}
for label in metadataManager.getAllLabels()
result.push {
caption: "\\#{commandName}{#{label}#{closingBrace}"
value: "\\#{commandName}{#{label}#{closingBrace}"
caption: "\\#{commandName}{#{label}}"
value: "\\#{commandName}{#{label}}"
meta: "cross-reference"
score: 50
}
@@ -118,17 +112,14 @@ define [
references = @$scope.$root._references
ReferencesCompleter =
getCompletions: (editor, session, pos, prefix, callback) ->
context = Helpers.getContext(editor, pos)
{commandFragment, closingBrace} = context
{commandFragment} = Helpers.getContext(editor, pos)
if commandFragment
citeMatch = commandFragment.match(
/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*, *)?(\w*)/
)
if citeMatch
commandName = citeMatch[1]
previousArgs = citeMatch[2]
currentArg = citeMatch[3]
if previousArgs == undefined
[_, commandName, previousArgs, currentArg] = citeMatch
if !previousArgs?
previousArgs = ""
previousArgsCaption = if previousArgs.length > 8 then "…," else previousArgs
result = []
@@ -140,10 +131,10 @@ define [
}
if references.keys and references.keys.length > 0
references.keys.forEach (key) ->
if !(key in [null, undefined])
if key?
result.push({
caption: "\\#{commandName}{#{previousArgsCaption}#{key}#{closingBrace}"
value: "\\#{commandName}{#{previousArgs}#{key}#{closingBrace}"
caption: "\\#{commandName}{#{previousArgsCaption}#{key}}"
value: "\\#{commandName}{#{previousArgs}#{key}}"
meta: "reference"
score: 50
})
@@ -170,8 +161,7 @@ define [
onChange: (change) ->
cursorPosition = @editor.getCursorPosition()
end = change.end
context = Helpers.getContext(@editor, end)
{lineUpToCursor, commandFragment} = context
{lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end)
if lineUpToCursor.match(/.*%.*/)
return
lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
@@ -188,10 +178,19 @@ define [
end.row == cursorPosition.row and
end.column == cursorPosition.column + 1
)
if (commandFragment? and commandFragment.length > 2) or lastCharIsBackslash
if commandFragment?.length > 2 or lastCharIsBackslash
setTimeout () =>
@editor.execCommand("startAutocomplete")
, 0
if (
change.action == "insert" and
change.lines[0].match(/\\(\w+){}/)?[1].match(
/(begin|end|[a-z]*ref|usepackage|[a-z]*cite[a-z]*|input|include)/
)
)
setTimeout () =>
@editor.execCommand("startAutocomplete")
, 0
monkeyPatchAutocomplete: () ->
Autocomplete = ace.require("ace/autocomplete").Autocomplete
@@ -209,7 +208,7 @@ define [
# If we are in \begin{it|}, then we need to remove the trailing }
# since it will be adding in with the autocomplete of \begin{item}...
if this.completions.filterText.match(/^\\begin\{/) and nextChar == "}"
if this.completions.filterText.match(/^\\\w+{/) and nextChar == "}"
editor.session.remove(range)
# Provide our own `insertMatch` implementation.
@@ -154,14 +154,14 @@ define [
totalArgs = squareArgsNo + curlyArgsNo
if totalArgs == 0
completionBeforeCursor = completionBase
completionAfterCurspr = ""
completionAfterCursor = ""
else
completionBeforeCursor = completionBase + args[0]
completionAfterCursor = args.slice(1)
return {
base: base,
completion: completionBase + args,
base: base
completion: completionBase + args
completionBeforeCursor: completionBeforeCursor
completionAfterCursor: completionAfterCursor
}
@@ -160,14 +160,10 @@ define () ->
hasDocumentEnvironment = (text) ->
re = /^\\begin{document}/m
envs = []
iterations = 0
return re.exec(text) != null
hasBibliographyEnvironment = (text) ->
re = /^\\begin{thebibliography}/m
envs = []
iterations = 0
return re.exec(text) != null
class EnvironmentManager
@@ -33,14 +33,11 @@ define [
commandName = Helpers.getCommandNameFromFragment(commandFragment)
beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/)
closingBrace = if needsClosingBrace then '}' else ''
return {
lineUpToCursor,
commandFragment,
commandName,
lineBeyondCursor,
closingBrace
lineBeyondCursor
}
return Helpers
@@ -1,6 +1,4 @@
define [
"./Helpers"
], (Helpers) ->
define [], () ->
packages = [
'inputenc', 'graphicx', 'amsmath', 'geometry', 'amssymb', 'hyperref',
'babel', 'color', 'xcolor', 'url', 'natbib', 'fontenc', 'fancyhdr',
@@ -26,14 +24,13 @@ define [
constructor: (@metadataManager) ->
getCompletions: (editor, session, pos, prefix, callback) ->
{closingBrace} = Helpers.getContext(editor, pos)
usedPackages = Object.keys(@metadataManager.getAllPackages())
packageSnippets = []
for pkg in packages
if pkg not in usedPackages
packageSnippets.push {
caption: "\\usepackage{#{pkg}#{closingBrace}"
snippet: "\\usepackage{#{pkg}#{closingBrace}"
caption: "\\usepackage{#{pkg}}"
snippet: "\\usepackage{#{pkg}}"
meta: "pkg"
}
@@ -73,6 +73,11 @@ define -> [{
"snippet": "\\documentclass{$1}",
"meta": "cmd",
"score": 1.4425339817971206
}, {
"caption": "\\ref{}",
"snippet": "\\ref{$1}",
"meta": "cross-reference",
"score": 0.014379554883991673
}, {
"caption": "\\frac{}{}",
"snippet": "\\frac{$1}{$2}",
@@ -4,10 +4,12 @@ define [
App.controller "FileTreeController", ["$scope", "$modal", "ide", "$rootScope", ($scope, $modal, ide, $rootScope) ->
$scope.openNewDocModal = () ->
$modal.open(
templateUrl: "newDocModalTemplate"
controller: "NewDocModalController"
templateUrl: "newFileModalTemplate"
controller: "NewFileModalController"
size: 'lg'
resolve: {
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
type: () -> 'doc'
}
)
@@ -22,37 +24,12 @@ define [
$scope.openUploadFileModal = () ->
$modal.open(
templateUrl: "uploadFileModalTemplate"
controller: "UploadFileModalController"
scope: $scope
resolve: {
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
}
)
$scope.openLinkedFileModal = window.openLinkedFileModal = () ->
unless 'url' in window.data.enabledLinkedFileTypes
console.warn("Url linked files are not enabled")
return
$modal.open(
templateUrl: "linkedFileModalTemplate"
controller: "LinkedFileModalController"
scope: $scope
resolve: {
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
}
)
$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
templateUrl: "newFileModalTemplate"
controller: "NewFileModalController"
size: 'lg'
resolve: {
parent_folder: () -> ide.fileTreeManager.getCurrentFolder()
type: () -> 'upload'
}
)
@@ -67,42 +44,10 @@ define [
$scope.$broadcast "delete:selected"
]
App.controller "NewDocModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->
$scope.inputs =
name: "name.tex"
$scope.state =
inflight: false
$modalInstance.opened.then () ->
$timeout () ->
$scope.$broadcast "open"
, 200
$scope.create = () ->
name = $scope.inputs.name
if !name? or name.length == 0
return
$scope.state.inflight = true
ide.fileTreeManager
.createDoc(name, parent_folder)
.then () ->
$scope.state.inflight = false
$modalInstance.close()
.catch (response)->
{ data } = response
$scope.error = data
$scope.state.inflight = false
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
]
App.controller "NewFolderModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->
$scope.inputs =
$scope.inputs =
name: "name"
$scope.state =
inflight: false
@@ -118,10 +63,10 @@ define [
return
$scope.state.inflight = true
ide.fileTreeManager
.createFolder(name, parent_folder)
.createFolder(name, $scope.parent_folder)
.then () ->
$scope.state.inflight = false
$modalInstance.close()
$modalInstance.dismiss('done')
.catch (response)->
{ data } = response
$scope.error = data
@@ -131,10 +76,60 @@ define [
$modalInstance.dismiss('cancel')
]
App.controller "NewFileModalController", [
"$scope", "type", "parent_folder", "$modalInstance"
($scope, type, parent_folder, $modalInstance) ->
$scope.type = type
$scope.parent_folder = parent_folder
$scope.state = {
inflight: false
valid: true
}
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
$scope.create = () ->
$scope.$broadcast 'create'
$scope.$on 'done', () ->
$modalInstance.dismiss('done')
]
App.controller "NewDocModalController", [
"$scope", "ide", "$timeout"
($scope, ide, $timeout) ->
$scope.inputs =
name: "name.tex"
validate = () ->
name = $scope.inputs.name
$scope.state.valid = (name? and name.length > 0)
$scope.$watch 'inputs.name', validate
$timeout () ->
$scope.$broadcast "open"
, 200
$scope.$on 'create', () ->
name = $scope.inputs.name
if !name? or name.length == 0
return
$scope.state.inflight = true
ide.fileTreeManager
.createDoc(name, $scope.parent_folder)
.then () ->
$scope.state.inflight = false
$scope.$emit 'done'
.catch (response)->
{ data } = response
$scope.error = data
$scope.state.inflight = false
]
App.controller "UploadFileModalController", [
"$scope", "$rootScope", "ide", "$modalInstance", "$timeout", "parent_folder", "$window"
($scope, $rootScope, ide, $modalInstance, $timeout, parent_folder, $window) ->
$scope.parent_folder_id = parent_folder?.id
"$scope", "$rootScope", "ide", "$timeout", "$window"
($scope, $rootScope, ide, $timeout, $window) ->
$scope.parent_folder_id = $scope.parent_folder?.id
$scope.project_id = ide.project_id
$scope.tooManyFiles = false
$scope.rateLimitHit = false
$scope.secondsToRedirect = 10
@@ -162,7 +157,7 @@ define [
if response.success
$rootScope.$broadcast 'file:upload:complete', response
if uploadCount == 0 and response? and response.success
$modalInstance.close("done")
$scope.$emit 'done'
), 250
$scope.onValidateBatch = (files)->
@@ -210,30 +205,44 @@ define [
$scope.doUpload = () ->
$scope.control?.q?.uploadStoredFiles()
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
]
App.controller "ProjectLinkedFileModalController", [
"$scope", "ide", "$modalInstance", "$timeout", "parent_folder",
($scope, ide, $modalInstance, $timeout, parent_folder) ->
"$scope", "ide", "$timeout",
($scope, ide, $timeout) ->
$scope.data =
projects: null # or []
selectedProjectId: null
projectEntities: null # or []
projectOutputFiles: null # or []
selectedProjectEntity: null
selectedProjectOutputFile: null
buildId: null
name: null
$scope.state =
inFlight:
projects: false
entities: false
create: false
error: false
$scope.state.inFlight =
projects: false
entities: false
compile: false
$scope.state.isOutputFilesMode = false
$scope.state.error = false
$scope.$watch 'data.selectedProjectId', (newVal, oldVal) ->
return if !newVal
$scope.data.selectedProjectEntity = null
$scope.getProjectEntities($scope.data.selectedProjectId)
$scope.data.selectedProjectOutputFile = null
if $scope.state.isOutputFilesMode
$scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId)
else
$scope.getProjectEntities($scope.data.selectedProjectId)
$scope.$watch 'state.isOutputFilesMode', (newVal, oldVal) ->
return if !newVal and !oldVal
$scope.data.selectedProjectOutputFile = null
if newVal == true
$scope.compileProjectAndGetOutputFiles($scope.data.selectedProjectId)
else
$scope.getProjectEntities($scope.data.selectedProjectId)
# auto-set filename based on selected file
$scope.$watch 'data.selectedProjectEntity', (newVal, oldVal) ->
@@ -242,15 +251,31 @@ define [
if fileName
$scope.data.name = fileName
# auto-set filename based on selected file
$scope.$watch 'data.selectedProjectOutputFile', (newVal, oldVal) ->
return if !newVal
if newVal == 'output.pdf'
project = _.find($scope.data.projects, (p) -> p._id == $scope.data.selectedProjectId)
$scope.data.name = if project?.name? then "#{project.name}.pdf" else 'output.pdf'
else
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
inFlight.projects = inFlight.entities = inFlight.compile = false
$scope.state.inflight = false
$scope.state.error = isError
$scope.toggleOutputFilesMode = () ->
return if !$scope.data.selectedProjectId
$scope.state.isOutputFilesMode = !$scope.state.isOutputFilesMode
$scope.shouldEnableProjectSelect = () ->
{ state, data } = $scope
return !state.inFlight.projects && data.projects
@@ -259,16 +284,33 @@ define [
{ state, data } = $scope
return !state.inFlight.projects && !state.inFlight.entities && data.projects && data.selectedProjectId
$scope.shouldEnableCreateButton = () ->
$scope.shouldEnableProjectOutputFileSelect = () ->
{ state, data } = $scope
return !state.inFlight.projects && !state.inFlight.compile && data.projects && data.selectedProjectId
validate = () ->
state = $scope.state
data = $scope.data
return !state.inFlight.projects &&
$scope.state.valid = !state.inFlight.projects &&
!state.inFlight.entities &&
data.projects &&
data.selectedProjectId &&
data.projectEntities &&
data.selectedProjectEntity &&
(
(
!$scope.state.isOutputFilesMode &&
data.projectEntities &&
data.selectedProjectEntity
) ||
(
$scope.state.isOutputFilesMode &&
data.projectOutputFiles &&
data.selectedProjectOutputFile
)
) &&
data.name
$scope.$watch 'state', validate, true
$scope.$watch 'data', validate, true
$scope.getUserProjects = () ->
_setInFlight('projects')
@@ -295,50 +337,84 @@ define [
.catch (err) ->
_reset(err: true)
$scope.compileProjectAndGetOutputFiles = (project_id) =>
_setInFlight('compile')
ide.$http.post("/project/#{project_id}/compile", {
check: "silent",
draft: false,
incrementalCompilesEnabled: false
_csrf: window.csrfToken
})
.then (resp) ->
if resp.data.status == 'success'
filteredFiles = resp.data.outputFiles.filter (f) ->
f.path.match(/.*\.(pdf|png|jpeg|jpg|gif)/)
$scope.data.projectOutputFiles = filteredFiles
$scope.data.buildId = filteredFiles?[0]?.build
console.log ">> build_id", $scope.data.buildId
_reset(err: false)
else
$scope.data.projectOutputFiles = null
_reset(err: true)
.catch (err) ->
console.error(err)
_reset(err: true)
$scope.init = () ->
$scope.getUserProjects()
$timeout($scope.init, 0)
$scope.create = () ->
$scope.$on 'create', () ->
projectId = $scope.data.selectedProjectId
path = $scope.data.selectedProjectEntity
name = $scope.data.name
if !name || !path || !projectId
_reset(err: true)
return
if $scope.state.isOutputFilesMode
provider = 'project_output_file'
payload = {
source_project_id: projectId,
source_output_file_path: $scope.data.selectedProjectOutputFile,
build_id: $scope.data.buildId
}
else
provider = 'project_file'
payload = {
source_project_id: projectId,
source_entity_path: $scope.data.selectedProjectEntity
}
_setInFlight('create')
ide.fileTreeManager
.createLinkedFile(name, parent_folder, 'project_file', {
source_project_id: projectId,
source_entity_path: path
})
.createLinkedFile(name, $scope.parent_folder, provider, payload)
.then () ->
_reset(err: false)
$modalInstance.close()
$scope.$emit 'done'
.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) ->
App.controller "UrlLinkedFileModalController", [
"$scope", "ide", "$timeout"
($scope, ide, $timeout) ->
$scope.inputs =
name: ""
url: ""
$scope.nameChangedByUser = false
$scope.state =
inflight: false
$modalInstance.opened.then () ->
$timeout () ->
$scope.$broadcast "open"
, 200
$timeout () ->
$scope.$broadcast "open"
, 200
validate = () ->
{name, url} = $scope.inputs
if !name? or name.length == 0
$scope.state.valid = false
else if !url? or url.length == 0
$scope.state.valid = false
else
$scope.state.valid = true
$scope.$watch 'inputs.name', validate
$scope.$watch 'inputs.url', validate
$scope.$watch "inputs.url", (url) ->
if url? and url != "" and !$scope.nameChangedByUser
@@ -347,7 +423,7 @@ define [
if parts.length > 1 # Wait for at one /
$scope.inputs.name = parts[0]
$scope.create = () ->
$scope.$on 'create', () ->
{name, url} = $scope.inputs
if !name? or name.length == 0
return
@@ -355,15 +431,13 @@ define [
return
$scope.state.inflight = true
ide.fileTreeManager
.createLinkedFile(name, parent_folder, 'url', {url})
.createLinkedFile(name, $scope.parent_folder, 'url', {url})
.then () ->
$scope.state.inflight = false
$modalInstance.close()
$scope.$emit 'done'
.catch (response)->
{ data } = response
$scope.error = data
$scope.state.inflight = false
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
]
@@ -4,12 +4,6 @@ define [
], (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"
@@ -153,6 +153,7 @@ define [
if $scope.shouldABTestPlans
sixpack.participate 'plans-details', ['default', 'more-details'], (chosenVariation, rawResponse)->
$scope.plansVariant = chosenVariation
event_tracking.send 'subscription-funnel', 'plans-page-loaded', chosenVariation
$scope.showPlans = true
@@ -261,3 +261,43 @@
}
}
}
.modal-new-file {
padding: 0;
table {
width: 100%;
td {
vertical-align: top;
}
}
.toggle-output-files-button {
font-size: 80%;
}
}
.modal-new-file--list {
background-color: @modal-footer-background-color;
width: 220px;
ul {
li {
padding: (@line-height-computed / 4);
a {
color: @text-color;
}
}
li.active {
background-color: white;
a {
color: @link-color;
}
}
}
}
.modal-new-file--body {
padding: 20px;
padding-top: (@line-height-computed / 4);
}
.modal-new-file--body-upload {
padding-top: 20px;
}
@@ -58,6 +58,10 @@
background-color: @sidebar-bg;
padding-top: @content-margin-vertical;
padding-bottom: @content-margin-vertical;
color: @sidebar-color;
.small {
color: @sidebar-color;
}
}
.project-list-sidebar when (@is-overleaf) {
@@ -109,9 +113,6 @@
}
p {
margin-bottom: @line-height-computed / 4;
&.small {
color: @sidebar-color;
}
}
}
@@ -65,3 +65,10 @@
.alert-danger {
.alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
}
.alert when (@is-overleaf = true) {
a {
color: white;
text-decoration: underline;
}
}
@@ -10,13 +10,17 @@
}
.qq-uploader-selector {
text-align: center;
.drag-here {
border: 1px dashed #666;
vertical-align: middle;
}
border: 1px dashed #666;
border-radius: 6px;
vertical-align: middle;
.help {
margin-top: 6px;
}
min-height: 200px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
}
/*.qq-upload-button-selector {
display: block;
@@ -0,0 +1,88 @@
expect = require("chai").expect
request = require './helpers/request'
Settings = require "settings-sharelatex"
auth = new Buffer('sharelatex:password').toString("base64")
authed_request = request.defaults
headers:
Authorization: "Basic #{auth}"
describe 'ApiClsiTests', ->
describe 'compile', ->
before (done) ->
@compileSpec =
compile:
options:
compiler: 'pdflatex'
timeout: 60
rootResourcePath: 'main.tex'
resources: [
path: 'main/tex'
content: "\\documentclass{article}\n\\begin{document}\nHello World\n\\end{document}"
,
path: 'image.png'
url: 'www.example.com/image.png'
modified: 123456789
]
done()
describe 'valid request', ->
it 'returns success and a list of output files', (done) ->
authed_request.post {
uri: '/api/clsi/compile/abcd'
json: @compileSpec
}, (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 200
expect(response.body).to.deep.equal {
status: 'success'
outputFiles: [
path: 'project.pdf'
url: '/project/abcd/build/1234/output/project.pdf'
type: 'pdf'
build: 1234
,
path: 'project.log'
url: '/project/abcd/build/1234/output/project.log'
type: 'log'
build: 1234
]
}
done()
describe 'unauthorized', ->
it 'returns 401', (done) ->
request.post {
uri: '/api/clsi/compile/abcd'
json: @compileSpec
}, (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 401
expect(response.body).to.equal 'Unauthorized'
done()
describe 'get output', ->
describe 'valid file', ->
it 'returns the file', (done) ->
authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 200
expect(response.body).to.equal 'mock-pdf'
done()
describe 'invalid file', ->
it 'returns 404', (done) ->
authed_request.get '/api/clsi/compile/abcd/build/1234/output/project.aux', (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 404
expect(response.body).to.not.equal 'mock-pdf'
done()
describe 'unauthorized', ->
it 'returns 401', (done) ->
request.get '/api/clsi/compile/abcd/build/1234/output/project.pdf', (error, response, body) ->
throw error if error?
expect(response.statusCode).to.equal 401
expect(response.body).to.not.equal 'mock-pdf'
done()
@@ -8,6 +8,8 @@ MockFileStoreApi = require './helpers/MockFileStoreApi'
request = require "./helpers/request"
User = require "./helpers/User"
MockClsiApi = require "./helpers/MockClsiApi"
express = require("express")
LinkedUrlProxy = express()
@@ -116,7 +118,6 @@ describe "LinkedFiles", ->
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()
@@ -149,7 +150,7 @@ describe "LinkedFiles", ->
source_entity_path: "/#{@source_doc_name}",
}, (error, response, body) =>
expect(response.statusCode).to.equal 403
expect(body).to.equal 'Cannot create linked file'
expect(body).to.equal 'You do not have access to this project'
done()
describe "with a linked project_file from a v1 project that has not been imported", ->
@@ -344,3 +345,105 @@ describe "LinkedFiles", ->
# TODO: Add test for asking for host that return ENOTFOUND
# (This will probably end up handled by the proxy)
describe "creating a linked output file", ->
before (done) ->
async.series [
(cb) =>
@owner.createProject 'output-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 'output-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)
], done
it 'should import the project.pdf file from the source project', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file",
json:
name: 'test.pdf',
parent_folder_id: @project_one_root_folder_id,
provider: 'project_output_file',
data:
source_project_id: @project_two_id,
source_output_file_path: "project.pdf",
build_id: '1234-abcd'
}, (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_output_file',
source_project_id: @project_two_id,
source_output_file_path: "project.pdf",
build_id: '1234-abcd'
}
expect(firstFile.name).to.equal('test.pdf')
done()
it 'should refresh the file', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file/#{@existing_file_id}/refresh",
json: true
}, (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
@refreshed_file_id = new_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.pdf')
done()
describe "with a linked project_output_file from a v1 project that has not been imported", ->
before (done) ->
async.series [
(cb) =>
@owner.createProject 'output-v1-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()
@project_one.rootFolder[0].fileRefs.push {
linkedFileData: {
provider: "project_output_file",
v1_source_doc_id: 9999999, # We won't find this id in the database
source_output_file_path: "project.pdf",
build_id: '123'
},
_id: "abcdef",
rev: 0,
created: new Date(),
name: "whatever.pdf"
}
@owner.saveProject @project_one, cb
], done
it 'should refuse to refresh', (done) ->
@owner.request.post {
url: "/project/#{@project_one_id}/linked_file/abcdef/refresh",
json: true
}, (error, response, body) =>
expect(response.statusCode).to.equal 409
expect(body).to.equal "Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file"
done()
@@ -0,0 +1,200 @@
expect = require("chai").expect
async = require("async")
User = require "./helpers/User"
request = require "./helpers/request"
settings = require "settings-sharelatex"
{db, ObjectId} = require("../../../app/js/infrastructure/mongojs")
MockV1Api = require "./helpers/MockV1Api"
describe "UserEmails", ->
beforeEach (done) ->
@timeout(20000)
@user = new User()
@user.login done
describe 'confirming an email', ->
it 'should confirm the email', (done) ->
token = null
async.series [
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails',
json:
email: 'newly-added-email@example.com'
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 204
cb()
(cb) =>
@user.request { url: '/user/emails', json: true }, (error, response, body) ->
expect(response.statusCode).to.equal 200
expect(body[0].confirmedAt).to.not.exist
expect(body[1].confirmedAt).to.not.exist
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should only be one confirmation token at the moment
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal 'newly-added-email@example.com'
expect(tokens[0].data.user_id).to.equal @user._id
token = tokens[0].token
cb()
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
token: token
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 200
cb()
(cb) =>
@user.request { url: '/user/emails', json: true }, (error, response, body) ->
expect(response.statusCode).to.equal 200
expect(body[0].confirmedAt).to.not.exist
expect(body[1].confirmedAt).to.exist
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# Token should be deleted after use
expect(tokens.length).to.equal 0
cb()
], done
it 'should not allow confirmation of the email if the user has changed', (done) ->
token1 = null
token2 = null
@user2 = new User()
@email = 'duplicate-email@example.com'
async.series [
(cb) => @user2.login cb
(cb) =>
# Create email for first user
@user.request {
method: 'POST',
url: '/user/emails',
json: {@email}
}, cb
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should only be one confirmation token at the moment
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal @email
expect(tokens[0].data.user_id).to.equal @user._id
token1 = tokens[0].token
cb()
(cb) =>
# Delete the email from the first user
@user.request {
method: 'DELETE',
url: '/user/emails',
json: {@email}
}, cb
(cb) =>
# Create email for second user
@user2.request {
method: 'POST',
url: '/user/emails',
json: {@email}
}, cb
(cb) =>
# Original confirmation token should no longer work
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
token: token1
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 404
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user2._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# The first token has been used, so this should be token2 now
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal @email
expect(tokens[0].data.user_id).to.equal @user2._id
token2 = tokens[0].token
cb()
(cb) =>
# Second user should be able to confirm the email
@user2.request {
method: 'POST',
url: '/user/emails/confirm',
json:
token: token2
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 200
cb()
(cb) =>
@user2.request { url: '/user/emails', json: true }, (error, response, body) ->
expect(response.statusCode).to.equal 200
expect(body[0].confirmedAt).to.not.exist
expect(body[1].confirmedAt).to.exist
cb()
], done
describe "with an expired token", ->
it 'should not confirm the email', (done) ->
token = null
async.series [
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails',
json:
email: @email = 'expired-token-email@example.com'
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 204
cb()
(cb) =>
db.tokens.find {
use: 'email_confirmation',
'data.user_id': @user._id,
usedAt: { $exists: false }
}, (error, tokens) =>
# There should only be one confirmation token at the moment
expect(tokens.length).to.equal 1
expect(tokens[0].data.email).to.equal @email
expect(tokens[0].data.user_id).to.equal @user._id
token = tokens[0].token
cb()
(cb) =>
db.tokens.update {
token: token
}, {
$set: {
expiresAt: new Date(Date.now() - 1000000)
}
}, cb
(cb) =>
@user.request {
method: 'POST',
url: '/user/emails/confirm',
json:
token: token
}, (error, response, body) =>
return done(error) if error?
expect(response.statusCode).to.equal 404
cb()
], done
@@ -0,0 +1,50 @@
express = require("express")
bodyParser = require "body-parser"
app = express()
module.exports = MockClsiApi =
run: () ->
compile = (req, res, next) =>
res.status(200).send {
compile:
status: 'success'
error: null
outputFiles: [
url: "/project/#{req.params.project_id}/build/1234/output/project.pdf"
path: 'project.pdf'
type: 'pdf'
build: 1234
,
url: "/project/#{req.params.project_id}/build/1234/output/project.log"
path: 'project.log'
type: 'log'
build: 1234
]
}
app.post "/project/:project_id/compile", compile
app.post "/project/:project_id/user/:user_id/compile", compile
app.get "/project/:project_id/build/:build_id/output/*", (req, res, next) ->
filename = req.params[0]
if filename == 'project.pdf'
res.status(200).send 'mock-pdf'
else if filename == 'project.log'
res.status(200).send 'mock-log'
else
res.sendStatus(404)
app.get "/project/:project_id/user/:user_id/build/:build_id/output/:output_path", (req, res, next) =>
res.status(200).send("hello")
app.get "/project/:project_id/status", (req, res, next) =>
res.status(200).send()
app.listen 3013, (error) ->
throw error if error?
.on "error", (error) ->
console.error "error starting MockClsiApi:", error.message
process.exit(1)
MockClsiApi.run()
@@ -42,6 +42,14 @@ module.exports = MockV1Api =
@exportParams = Object.assign({}, req.body)
res.json exportId: @exportId
app.get "/api/v2/users/:userId/affiliations", (req, res, next) =>
res.json []
app.post "/api/v2/users/:userId/affiliations", (req, res, next) =>
res.sendStatus 201
app.delete "/api/v2/users/:userId/affiliations/:email", (req, res, next) =>
res.sendStatus 204
app.listen 5000, (error) ->
throw error if error?
@@ -8,7 +8,7 @@ SandboxedModule = require('sandboxed-module')
describe "ClsiManager", ->
beforeEach ->
@jar = {cookie:"stuff"}
@ClsiCookieManager =
@ClsiCookieManager =
getCookieJar: sinon.stub().callsArgWith(1, null, @jar)
setServerId: sinon.stub().callsArgWith(2)
_getServerId:sinon.stub()
@@ -99,8 +99,8 @@ describe "ClsiManager", ->
status: @status = "failure"
})
@ClsiManager.sendRequest @project_id, @user_id, {}, @callback
it "should call the callback with a failure statue", ->
it "should call the callback with a failure status", ->
@callback.calledWith(null, @status).should.equal true
describe "with a sync conflict", ->
@@ -137,11 +137,82 @@ describe "ClsiManager", ->
it "should call the callback with an error", ->
@callback.calledWithExactly(new Error("failed")).should.equal true
describe "sendExternalRequest", ->
beforeEach ->
@submission_id = "submission-id"
@clsi_request = "mock-request"
@ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3")
describe "with a successful compile", ->
beforeEach ->
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
compile:
status: @status = "success"
outputFiles: [{
url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.pdf"
path: "output.pdf"
type: "pdf"
build: 1234
},{
url: "#{@settings.apis.clsi.url}/project/#{@submission_id}/build/1234/output/output.log"
path: "output.log"
type: "log"
build: 1234
}]
})
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {compileGroup:"standard"}, @callback
it "should send the request to the CLSI", ->
@ClsiManager._postToClsi
.calledWith(@submission_id, null, @clsi_request, "standard")
.should.equal true
it "should call the callback with the status and output files", ->
outputFiles = [{
url: "/project/#{@submission_id}/build/1234/output/output.pdf"
path: "output.pdf"
type: "pdf"
build: 1234
},{
url: "/project/#{@submission_id}/build/1234/output/output.log"
path: "output.log"
type: "log"
build: 1234
}]
@callback.calledWith(null, @status, outputFiles).should.equal true
describe "with a failed compile", ->
beforeEach ->
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
compile:
status: @status = "failure"
})
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback
it "should call the callback with a failure status", ->
@callback.calledWith(null, @status).should.equal true
describe "when the resources fail the precompile check", ->
beforeEach ->
@ClsiFormatChecker.checkRecoursesForProblems = sinon.stub().callsArgWith(1, new Error("failed"))
@ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
compile:
status: @status = "failure"
})
@ClsiManager.sendExternalRequest @submission_id, @clsi_request, {}, @callback
it "should call the callback only once", ->
@callback.calledOnce.should.equal true
it "should call the callback with an error", ->
@callback.calledWithExactly(new Error("failed")).should.equal true
describe "deleteAuxFiles", ->
beforeEach ->
@ClsiManager._makeRequest = sinon.stub().callsArg(2)
@DocumentUpdaterHandler.clearProjectState = sinon.stub().callsArg(1)
describe "with the standard compileGroup", ->
beforeEach ->
@ClsiManager.deleteAuxFiles @project_id, @user_id, {compileGroup: "standard"}, @callback
@@ -158,7 +229,7 @@ describe "ClsiManager", ->
it "should call the callback", ->
@callback.called.should.equal true
describe "_buildRequest", ->
beforeEach ->
@@ -441,7 +512,7 @@ describe "ClsiManager", ->
it "should call the callback", ->
@callback.called.should.equal true
describe "with param file", ->
beforeEach ->
@ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback
@@ -450,7 +521,7 @@ describe "ClsiManager", ->
@ClsiManager._makeRequest
.calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:undefined}})
.should.equal true
describe "with image", ->
beforeEach ->
@req.compile.options.imageName = @image = "example.com/mock/image"
@@ -468,7 +539,7 @@ describe "ClsiManager", ->
beforeEach ->
@response = {there:"something"}
@request.callsArgWith(1, null, @response)
@opts =
@opts =
method: "SOMETHIGN"
url: "http://a place on the web"
@@ -29,6 +29,9 @@ describe "CompileController", ->
url: "clsi.example.com"
clsi_priority:
url: "clsi-priority.example.com"
defaultFeatures:
compileGroup: 'standard'
compileTimeout: 60
@jar = {cookie:"stuff"}
@ClsiCookieManager =
getCookieJar:sinon.stub().callsArgWith(1, null, @jar)
@@ -109,6 +112,62 @@ describe "CompileController", ->
.calledWith(@project_id, @user_id, { isAutoCompile: false, draft: true })
.should.equal true
describe "compileSubmission", ->
beforeEach ->
@submission_id = 'sub-1234'
@req.params =
submission_id: @submission_id
@req.body = {}
@ClsiManager.sendExternalRequest = sinon.stub()
.callsArgWith(3, null, @status = "success", @outputFiles = ["mock-output-files"], \
@clsiServerId = "mock-server-id", @validationProblems = null)
it "should set the content-type of the response to application/json", ->
@CompileController.compileSubmission @req, @res, @next
@res.contentType
.calledWith("application/json")
.should.equal true
it "should send a successful response reporting the status and files", ->
@CompileController.compileSubmission @req, @res, @next
@res.statusCode.should.equal 200
@res.body.should.equal JSON.stringify({
status: @status
outputFiles: @outputFiles
clsiServerId: 'mock-server-id'
validationProblems: null
})
describe "with compileGroup and timeout", ->
beforeEach ->
@req.body =
compileGroup: 'special'
timeout: 600
@CompileController.compileSubmission @req, @res, @next
it "should use the supplied values", ->
@ClsiManager.sendExternalRequest
.calledWith(@submission_id, { compileGroup: 'special', timeout: 600 }, \
{ compileGroup: 'special', timeout: 600 })
.should.equal true
describe "with other supported options but not compileGroup and timeout", ->
beforeEach ->
@req.body =
rootResourcePath: 'main.tex'
compiler: 'lualatex'
draft: true
check: 'validate'
@CompileController.compileSubmission @req, @res, @next
it "should use the other options but default values for compileGroup and timeout", ->
@ClsiManager.sendExternalRequest
.calledWith(@submission_id, \
{rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate'}, \
{rootResourcePath: 'main.tex', compiler: 'lualatex', draft: true, check: 'validate', \
compileGroup: 'standard', timeout: 60})
.should.equal true
describe "downloadPdf", ->
beforeEach ->
@req.params =
@@ -167,6 +226,38 @@ describe "CompileController", ->
done()
@CompileController.downloadPdf @req, @res
describe "getFileFromClsiWithoutUser", ->
beforeEach ->
@submission_id = 'sub-1234'
@build_id = 123456
@file = 'project.pdf'
@req.params =
submission_id: @submission_id
build_id: @build_id
file: @file
@req.body = {}
@expected_url = "/project/#{@submission_id}/build/#{@build_id}/output/#{@file}"
@CompileController.proxyToClsiWithLimits = sinon.stub()
describe "without limits specified", ->
beforeEach ->
@CompileController.getFileFromClsiWithoutUser @req, @res, @next
it "should proxy to CLSI with correct URL and default limits", ->
@CompileController.proxyToClsiWithLimits
.calledWith(@submission_id, @expected_url, {compileGroup: 'standard'})
.should.equal true
describe "with limits specified", ->
beforeEach ->
@req.body = {compileTimeout: 600, compileGroup: 'special'}
@CompileController.getFileFromClsiWithoutUser @req, @res, @next
it "should proxy to CLSI with correct URL and specified limits", ->
@CompileController.proxyToClsiWithLimits
.calledWith(@submission_id, @expected_url, {compileGroup: 'special'})
.should.equal true
describe "proxyToClsi", ->
beforeEach ->
@request.returns(@proxy = {
@@ -41,7 +41,7 @@ describe "PasswordResetHandler", ->
it "should check the user exists", (done)->
@UserGetter.getUserByMainEmail.callsArgWith(1)
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
@OneTimeTokenHandler.getNewToken.yields()
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
exists.should.equal false
done()
@@ -50,7 +50,7 @@ describe "PasswordResetHandler", ->
it "should send the email with the token", (done)->
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
@OneTimeTokenHandler.getNewToken.callsArgWith(1, null, @token)
@OneTimeTokenHandler.getNewToken.yields(null, @token)
@EmailHandler.sendEmail.callsArgWith(2)
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
@EmailHandler.sendEmail.called.should.equal true
@@ -63,7 +63,7 @@ describe "PasswordResetHandler", ->
it "should return exists = false for a holdingAccount", (done) ->
@user.holdingAccount = true
@UserGetter.getUserByMainEmail.callsArgWith(1, null, @user)
@OneTimeTokenHandler.getNewToken.callsArgWith(1)
@OneTimeTokenHandler.getNewToken.yields()
@PasswordResetHandler.generateAndEmailResetToken @user.email, (err, exists)=>
exists.should.equal false
done()
@@ -71,14 +71,14 @@ describe "PasswordResetHandler", ->
describe "setNewUserPassword", ->
it "should return false if no user id can be found", (done)->
@OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1)
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields()
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found) =>
found.should.equal false
@AuthenticationManager.setUserPassword.called.should.equal false
done()
it "should set the user password", (done)->
@OneTimeTokenHandler.getValueFromTokenAndExpire.callsArgWith(1, null, @user_id)
@OneTimeTokenHandler.getValueFromTokenAndExpire.yields(null, @user_id)
@AuthenticationManager.setUserPassword.callsArgWith(2)
@PasswordResetHandler.setNewUserPassword @token, @password, (err, found, user_id) =>
found.should.equal true
@@ -5,6 +5,7 @@ path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/Project/ProjectController"
expect = require("chai").expect
Errors = require "../../../../app/js/Features/Errors/Errors"
describe "ProjectController", ->
@@ -100,6 +101,7 @@ describe "ProjectController", ->
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
"../../infrastructure/Modules": @Modules
"./ProjectEntityHandler": @ProjectEntityHandler
"../Errors/Errors": Errors
@projectName = "£12321jkj9ujkljds"
@req =
@@ -303,6 +305,20 @@ describe "ProjectController", ->
done()
@ProjectController.projectListPage @req, @res
it 'should send hasSubscription == false when no subscription', (done) ->
@res.render = (pageName, opts)=>
opts.hasSubscription.should.equal false
done()
@ProjectController.projectListPage @req, @res
it 'should send hasSubscription == true when there is a subscription', (done) ->
@LimitationsManager.userHasSubscriptionOrIsGroupMember = sinon.stub().callsArgWith(1, null, true)
@res.render = (pageName, opts)=>
opts.hasSubscription.should.equal true
done()
@ProjectController.projectListPage @req, @res
describe 'front widget', (done) ->
beforeEach ->
@settings.overleaf =
@@ -44,6 +44,8 @@ describe 'ProjectEntityUpdateHandler', ->
else
@._id = file_id
@rev = 0
if options.linkedFileData?
@linkedFileData = options.linkedFileData
@docName = "doc-name"
@docLines = ['1234','abc']
@@ -121,6 +123,35 @@ describe 'ProjectEntityUpdateHandler', ->
.calledWithMatch(project_id, projectHistoryId, userId, changesMatcher)
.should.equal true
describe 'copyFileFromExistingProjectWithProject, with linkedFileData', ->
beforeEach ->
@oldProject_id = "123kljadas"
@oldFileRef = {
_id:"oldFileRef",
name:@fileName,
linkedFileData: @linkedFileData
}
@ProjectEntityMongoUpdateHandler._confirmFolder = sinon.stub().yields(folder_id)
@ProjectEntityMongoUpdateHandler._putElement = sinon.stub().yields(null, {path:{fileSystem: @fileSystemPath}})
@ProjectEntityUpdateHandler.copyFileFromExistingProjectWithProject @project, folder_id, @oldProject_id, @oldFileRef, userId, @callback
it 'should copy the file in FileStoreHandler', ->
@FileStoreHandler.copyFile
.calledWith(@oldProject_id, @oldFileRef._id, project_id, file_id)
.should.equal true
it 'should put file into folder by calling put element, with the linkedFileData', ->
@ProjectEntityMongoUpdateHandler._putElement
.calledWithMatch(
@project,
folder_id,
{ _id: file_id, name: @fileName, linkedFileData: @linkedFileData},
"file"
)
.should.equal true
describe 'updateDocLines', ->
beforeEach ->
@path = "/somewhere/something.tex"
@@ -285,7 +316,7 @@ describe 'ProjectEntityUpdateHandler', ->
beforeEach ->
@path = "/path/to/file"
@newFile = {_id: file_id, rev: 0, name: @fileName}
@newFile = {_id: file_id, rev: 0, name: @fileName, linkedFileData: @linkedFileData}
@TpdsUpdateSender.addFile = sinon.stub().yields()
@ProjectEntityMongoUpdateHandler.addFile = sinon.stub().yields(null, {path: fileSystem: @path}, @project)
@ProjectEntityUpdateHandler.addFile project_id, folder_id, @fileName, @fileSystemPath, @linkedFileData, userId, @callback
@@ -330,7 +361,7 @@ describe 'ProjectEntityUpdateHandler', ->
@newFileUrl = "new-file-url"
@FileStoreHandler.uploadFileFromDisk = sinon.stub().yields(null, @newFileUrl)
@newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0
@newFile = _id: new_file_id, name: "dummy-upload-filename", rev: 0, linkedFileData: @linkedFileData
@oldFile = _id: file_id
@path = "/path/to/file"
@ProjectEntityMongoUpdateHandler._insertDeletedFileReference = sinon.stub().yields()
@@ -5,64 +5,99 @@ path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/Security/OneTimeTokenHandler"
expect = require("chai").expect
Errors = require "../../../../app/js/Features/Errors/Errors"
tk = require("timekeeper")
describe "OneTimeTokenHandler", ->
beforeEach ->
@value = "user id here"
@stubbedToken = require("crypto").randomBytes(32)
@settings =
redis:
web:{}
@redisMulti =
set:sinon.stub()
get:sinon.stub()
del:sinon.stub()
expire:sinon.stub()
exec:sinon.stub()
self = @
tk.freeze Date.now() # freeze the time for these tests
@stubbedToken = "mock-token"
@callback = sinon.stub()
@OneTimeTokenHandler = SandboxedModule.require modulePath, requires:
"../../infrastructure/RedisWrapper" :
client: =>
auth:->
multi: -> return self.redisMulti
"settings-sharelatex":@settings
"logger-sharelatex": log:->
"crypto": randomBytes: () => @stubbedToken
"../Errors/Errors": Errors
"../../infrastructure/mongojs": db: @db = tokens: {}
afterEach ->
tk.reset()
describe "getNewToken", ->
beforeEach ->
@db.tokens.insert = sinon.stub().yields()
it "should set a new token into redis with a ttl", (done)->
@redisMulti.exec.callsArgWith(0)
@OneTimeTokenHandler.getNewToken @value, (err, token) =>
@redisMulti.set.calledWith("password_token:#{@stubbedToken.toString("hex")}", @value).should.equal true
@redisMulti.expire.calledWith("password_token:#{@stubbedToken.toString("hex")}", 60 * 60).should.equal true
done()
describe 'normally', ->
beforeEach ->
@OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', @callback
it "should return if there was an error", (done)->
@redisMulti.exec.callsArgWith(0, "error")
@OneTimeTokenHandler.getNewToken @value, (err, token)=>
err.should.exist
done()
it "should insert a generated token with a 1 hour expiry", ->
@db.tokens.insert
.calledWith({
use: 'password'
token: @stubbedToken,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
data: 'mock-data-to-store'
})
.should.equal true
it "should allow the expiry time to be overridden", (done) ->
@redisMulti.exec.callsArgWith(0)
@ttl = 42
@OneTimeTokenHandler.getNewToken @value, {expiresIn: @ttl}, (err, token) =>
@redisMulti.expire.calledWith("password_token:#{@stubbedToken.toString("hex")}", @ttl).should.equal true
done()
it 'should call the callback with the token', ->
@callback.calledWith(null, @stubbedToken).should.equal true
describe 'with an optional expiresIn parameter', ->
beforeEach ->
@OneTimeTokenHandler.getNewToken 'password', 'mock-data-to-store', { expiresIn: 42 }, @callback
it "should insert a generated token with a custom expiry", ->
@db.tokens.insert
.calledWith({
use: 'password'
token: @stubbedToken,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 42 * 1000)
data: 'mock-data-to-store'
})
.should.equal true
it 'should call the callback with the token', ->
@callback.calledWith(null, @stubbedToken).should.equal true
describe "getValueFromTokenAndExpire", ->
describe 'successfully', ->
beforeEach ->
@db.tokens.findAndModify = sinon.stub().yields(null, { data: 'mock-data' })
@OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback
it 'should expire the token', ->
@db.tokens.findAndModify
.calledWith({
query: {
use: 'password'
token: 'mock-token',
expiresAt: { $gt: new Date() },
usedAt: { $exists: false }
},
update: {
$set: { usedAt: new Date() }
}
})
.should.equal true
it 'should return the data', ->
@callback.calledWith(null, 'mock-data').should.equal true
describe 'when a valid token is not found', ->
beforeEach ->
@db.tokens.findAndModify = sinon.stub().yields(null, null)
@OneTimeTokenHandler.getValueFromTokenAndExpire 'password', 'mock-token', @callback
it 'should return a NotFoundError', ->
@callback
.calledWith(sinon.match.instanceOf(Errors.NotFoundError))
.should.equal true
it "should get and delete the token", (done)->
@redisMulti.exec.callsArgWith(0, null, [@value])
@OneTimeTokenHandler.getValueFromTokenAndExpire @stubbedToken, (err, value)=>
value.should.equal @value
@redisMulti.get.calledWith("password_token:#{@stubbedToken}").should.equal true
@redisMulti.del.calledWith("password_token:#{@stubbedToken}").should.equal true
done()
@@ -175,6 +175,22 @@ describe "TeamInvitesHandler", ->
).should.equal true
done()
describe "importInvite", ->
beforeEach ->
@sentAt = new Date()
it "can imports an invite from v1", ->
@TeamInvitesHandler.importInvite @subscription, "A-Team", "hannibal@a-team.org",
"secret", @sentAt, (error) =>
expect(error).not.to.exist
@subscription.save.calledOnce.should.eq true
invite = @subscription.teamInvites.find (i) -> i.email == "hannibal@a-team.org"
expect(invite.token).to.eq("secret")
expect(invite.sentAt).to.eq(@sentAt)
describe "acceptInvite", ->
beforeEach ->
@user = {
@@ -0,0 +1,125 @@
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/UserEmailsConfirmationHandler"
expect = require("chai").expect
Errors = require "../../../../app/js/Features/Errors/Errors"
EmailHelper = require "../../../../app/js/Features/Helpers/EmailHelper"
describe "UserEmailsConfirmationHandler", ->
beforeEach ->
@UserEmailsConfirmationHandler = SandboxedModule.require modulePath, requires:
"settings-sharelatex": @settings =
siteUrl: "emails.example.com"
"logger-sharelatex": @logger = { log: sinon.stub() }
"../Security/OneTimeTokenHandler": @OneTimeTokenHandler = {}
"../Errors/Errors": Errors
"./UserUpdater": @UserUpdater = {}
"../Email/EmailHandler": @EmailHandler = {}
"../Helpers/EmailHelper": EmailHelper
@user_id = "mock-user-id"
@email = "mock@example.com"
@callback = sinon.stub()
describe "sendConfirmationEmail", ->
beforeEach ->
@OneTimeTokenHandler.getNewToken = sinon.stub().yields(null, @token = "new-token")
@EmailHandler.sendEmail = sinon.stub().yields()
describe 'successfully', ->
beforeEach ->
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, @callback
it "should generate a token for the user which references their id and email", ->
@OneTimeTokenHandler.getNewToken
.calledWith(
'email_confirmation',
{@user_id, @email},
{ expiresIn: 365 * 24 * 60 * 60 }
)
.should.equal true
it 'should send an email to the user', ->
@EmailHandler.sendEmail
.calledWith('confirmEmail', {
to: @email,
confirmEmailUrl: 'emails.example.com/user/emails/confirm?token=new-token'
})
.should.equal true
it 'should call the callback', ->
@callback.called.should.equal true
describe 'with invalid email', ->
beforeEach ->
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, '!"£$%^&*()', @callback
it 'should return an error', ->
@callback.calledWith(sinon.match.instanceOf(Error)).should.equal true
describe 'a custom template', ->
beforeEach ->
@UserEmailsConfirmationHandler.sendConfirmationEmail @user_id, @email, 'myCustomTemplate', @callback
it 'should send an email with the given template', ->
@EmailHandler.sendEmail
.calledWith('myCustomTemplate')
.should.equal true
describe "confirmEmailFromToken", ->
beforeEach ->
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
null,
{@user_id, @email}
)
@UserUpdater.confirmEmail = sinon.stub().yields()
describe "successfully", ->
beforeEach ->
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
it "should call getValueFromTokenAndExpire", ->
@OneTimeTokenHandler.getValueFromTokenAndExpire
.calledWith('email_confirmation', @token)
.should.equal true
it "should confirm the email of the user_id", ->
@UserUpdater.confirmEmail
.calledWith(@user_id, @email)
.should.equal true
it "should call the callback", ->
@callback.called.should.equal true
describe 'with an expired token', ->
beforeEach ->
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(null, null)
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
it "should call the callback with a NotFoundError", ->
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
describe 'with no user_id in the token', ->
beforeEach ->
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
null,
{@email}
)
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
it "should call the callback with a NotFoundError", ->
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
describe 'with no email in the token', ->
beforeEach ->
@OneTimeTokenHandler.getValueFromTokenAndExpire = sinon.stub().yields(
null,
{@user_id}
)
@UserEmailsConfirmationHandler.confirmEmailFromToken @token = 'mock-token', @callback
it "should call the callback with a NotFoundError", ->
@callback.calledWith(sinon.match.instanceOf(Errors.NotFoundError)).should.equal true
@@ -8,6 +8,7 @@ modulePath = "../../../../app/js/Features/User/UserEmailsController.js"
SandboxedModule = require('sandboxed-module')
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
Errors = require("../../../../app/js/Features/Errors/Errors")
describe "UserEmailsController", ->
beforeEach ->
@@ -30,6 +31,8 @@ describe "UserEmailsController", ->
"./UserGetter": @UserGetter
"./UserUpdater": @UserUpdater
"../Helpers/EmailHelper": @EmailHelper
"./UserEmailsConfirmationHandler": @UserEmailsConfirmationHandler = {}
"../Errors/Errors": Errors
"logger-sharelatex":
log: -> console.log(arguments)
err: ->
@@ -47,47 +50,48 @@ describe "UserEmailsController", ->
assertCalledWith @UserGetter.getUserFullEmails, @user._id
done()
it 'handles error', (done) ->
@UserGetter.getUserFullEmails.callsArgWith 1, new Error('Oups')
@UserEmailsController.list @req,
sendStatus: (code) =>
code.should.equal 500
done()
describe 'Add', ->
beforeEach ->
@newEmail = 'new_email@baz.com'
@req.body.email = @newEmail
@req.body =
email: @newEmail
university: { name: 'University Name' }
department: 'Department'
role: 'Role'
@EmailHelper.parseEmail.returns @newEmail
@UserEmailsConfirmationHandler.sendConfirmationEmail = sinon.stub().yields()
@UserUpdater.addEmailAddress.callsArgWith 3, null
it 'adds new email', (done) ->
@UserUpdater.addEmailAddress.callsArgWith 2, null
@UserEmailsController.add @req,
sendStatus: (code) =>
code.should.equal 200
code.should.equal 204
assertCalledWith @EmailHelper.parseEmail, @newEmail
assertCalledWith @UserUpdater.addEmailAddress, @user._id, @newEmail
affiliationOptions = @UserUpdater.addEmailAddress.lastCall.args[2]
Object.keys(affiliationOptions).length.should.equal 3
affiliationOptions.university.should.equal @req.body.university
affiliationOptions.department.should.equal @req.body.department
affiliationOptions.role.should.equal @req.body.role
done()
it 'sends an email confirmation', (done) ->
@UserEmailsController.add @req,
sendStatus: (code) =>
code.should.equal 204
assertCalledWith @UserEmailsConfirmationHandler.sendConfirmationEmail, @user._id, @newEmail
done()
it 'handles email parse error', (done) ->
@EmailHelper.parseEmail.returns null
@UserEmailsController.add @req,
sendStatus: (code) =>
code.should.equal 422
assertNotCalled @UserUpdater.addEmailAddress
done()
it 'handles error', (done) ->
@UserUpdater.addEmailAddress.callsArgWith 2, new Error('Oups')
@UserEmailsController.add @req,
sendStatus: (code) =>
code.should.equal 500
done()
describe 'remove', ->
beforeEach ->
@email = 'email_to_remove@bar.com'
@@ -113,15 +117,6 @@ describe "UserEmailsController", ->
assertNotCalled @UserUpdater.removeEmailAddress
done()
it 'handles error', (done) ->
@UserUpdater.removeEmailAddress.callsArgWith 2, new Error('Oups')
@UserEmailsController.remove @req,
sendStatus: (code) =>
code.should.equal 500
done()
describe 'setDefault', ->
beforeEach ->
@email = "email_to_set_default@bar.com"
@@ -147,11 +142,50 @@ describe "UserEmailsController", ->
assertNotCalled @UserUpdater.setDefaultEmailAddress
done()
it 'handles error', (done) ->
@UserUpdater.setDefaultEmailAddress.callsArgWith 2, new Error('Oups')
describe 'confirm', ->
beforeEach ->
@UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields()
@res =
sendStatus: sinon.stub()
json: sinon.stub()
@res.status = sinon.stub().returns(@res)
@next = sinon.stub()
@token = 'mock-token'
@req.body = token: @token
describe 'successfully', ->
beforeEach ->
@UserEmailsController.confirm @req, @res, @next
it 'should confirm the email from the token', ->
@UserEmailsConfirmationHandler.confirmEmailFromToken
.calledWith(@token)
.should.equal true
it 'should return a 200 status', ->
@res.sendStatus.calledWith(200).should.equal true
describe 'without a token', ->
beforeEach ->
@req.body.token = null
@UserEmailsController.confirm @req, @res, @next
it 'should return a 422 status', ->
@res.sendStatus.calledWith(422).should.equal true
describe 'when confirming fails', ->
beforeEach ->
@UserEmailsConfirmationHandler.confirmEmailFromToken = sinon.stub().yields(
new Errors.NotFoundError('not found')
)
@UserEmailsController.confirm @req, @res, @next
it 'should return a 404 error code with a message', ->
@res.status.calledWith(404).should.equal true
@res.json.calledWith({
message: 'Sorry, your confirmation token is invalid or has expired. Please request a new email confirmation link.'
}).should.equal true
@UserEmailsController.setDefault @req,
sendStatus: (code) =>
code.should.equal 500
done()
@@ -20,11 +20,15 @@ describe "UserGetter", ->
@Mongo =
db: users: findOne: @findOne
ObjectId: (id) -> return id
settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@request = sinon.stub()
@UserGetter = SandboxedModule.require modulePath, requires:
"logger-sharelatex": log:->
"../../infrastructure/mongojs": @Mongo
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
'settings-sharelatex': settings
'request': @request
describe "getUser", ->
it "should get user", (done)->
@@ -42,6 +46,9 @@ describe "UserGetter", ->
done()
describe "getUserFullEmails", -
beforeEach ->
@request.callsArgWith(1, null, { statusCode: 200 }, [])
it "should get user", (done)->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser)
projection = email: 1, emails: 1
@@ -59,6 +66,33 @@ describe "UserGetter", ->
]
done()
it "should merge affiliation data", (done)->
@UserGetter.getUser = sinon.stub().callsArgWith(2, null, @fakeUser)
affiliationsData = [
{
email: 'email1@foo.bar'
role: 'Prof'
department: 'Maths'
inferred: false
institution: { name: 'University Name', isUniversity: true }
}
]
@request.callsArgWith(1, null, { statusCode: 200 }, affiliationsData)
@UserGetter.getUserFullEmails @fakeUser._id, (error, fullEmails) =>
assert.deepEqual fullEmails, [
{
email: 'email1@foo.bar'
default: false
affiliation:
institution: affiliationsData[0].institution
inferred: affiliationsData[0].inferred
department: affiliationsData[0].department
role: affiliationsData[0].role
}
{ email: 'email2@foo.bar', default: true }
]
done()
describe "getUserbyMainEmail", ->
it "query user by main email", (done)->
email = 'hello@world.com'
@@ -152,7 +152,7 @@ describe "UserRegistrationHandler", ->
beforeEach ->
@email = "email@example.com"
@crypto.randomBytes = sinon.stub().returns({toString: () => @password = "mock-password"})
@OneTimeTokenHandler.getNewToken.callsArgWith(2, null, @token = "mock-token")
@OneTimeTokenHandler.getNewToken.yields(null, @token = "mock-token")
@handler.registerNewUser = sinon.stub()
@callback = sinon.stub()
@@ -171,7 +171,7 @@ describe "UserRegistrationHandler", ->
it "should generate a new password reset token", ->
@OneTimeTokenHandler.getNewToken
.calledWith(@user_id, expiresIn: 7 * 24 * 60 * 60)
.calledWith('password', @user_id, expiresIn: 7 * 24 * 60 * 60)
.should.equal true
it "should send a registered email", ->
@@ -5,12 +5,12 @@ path = require('path')
sinon = require('sinon')
modulePath = path.join __dirname, "../../../../app/js/Features/User/UserUpdater"
expect = require("chai").expect
tk = require('timekeeper')
describe "UserUpdater", ->
beforeEach ->
@settings = {}
tk.freeze(Date.now())
@mongojs =
db:{}
ObjectId:(id)-> return id
@@ -19,12 +19,15 @@ describe "UserUpdater", ->
getUserByAnyEmail: sinon.stub()
ensureUniqueEmailAddress: sinon.stub()
@logger = err: sinon.stub(), log: ->
settings = apis: { v1: { url: 'v1.url', user: '', pass: '' } }
@request = sinon.stub()
@UserUpdater = SandboxedModule.require modulePath, requires:
"settings-sharelatex":@settings
"logger-sharelatex": @logger
"./UserGetter": @UserGetter
"../../infrastructure/mongojs":@mongojs
"metrics-sharelatex": timeAsyncMethod: sinon.stub()
'settings-sharelatex': settings
'request': @request
@stubbedUser =
_id: "3131231"
@@ -32,6 +35,9 @@ describe "UserUpdater", ->
email:"hello@world.com"
@newEmail = "bob@bob.com"
afterEach ->
tk.reset()
describe 'changeEmailAddress', ->
beforeEach ->
@UserGetter.getUserEmail.callsArgWith(1, null, @stubbedUser.email)
@@ -62,10 +68,10 @@ describe "UserUpdater", ->
describe 'addEmailAddress', ->
beforeEach ->
@UserGetter.ensureUniqueEmailAddress = sinon.stub().callsArgWith(1)
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null)
@request.callsArgWith(1, null, { statusCode: 201 })
it 'add email', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null)
@UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=>
@UserGetter.ensureUniqueEmailAddress.called.should.equal true
should.not.exist(err)
@@ -75,6 +81,27 @@ describe "UserUpdater", ->
).should.equal true
done()
it 'add affiliation', (done)->
affiliationOptions =
university: { id: 1 }
role: 'Prof'
department: 'Math'
@UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, affiliationOptions, (err)=>
should.not.exist(err)
@request.calledOnce.should.equal true
requestOptions = @request.lastCall.args[0]
expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations"
requestOptions.url.should.equal expectedUrl
requestOptions.method.should.equal 'POST'
body = requestOptions.body
Object.keys(body).length.should.equal 4
body.email.should.equal @newEmail
body.university.should.equal affiliationOptions.university
body.department.should.equal affiliationOptions.department
body.role.should.equal affiliationOptions.role
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@@ -83,10 +110,21 @@ describe "UserUpdater", ->
should.exist(err)
done()
describe 'removeEmailAddress', ->
it 'remove email', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
it 'handle affiliation error', (done)->
body = errors: 'affiliation error message'
@request.callsArgWith(1, null, { statusCode: 422 }, body)
@UserUpdater.addEmailAddress @stubbedUser._id, @newEmail, (err)=>
err.message.should.have.string 422
err.message.should.have.string body.errors
@UserUpdater.updateUser.called.should.equal false
done()
describe 'removeEmailAddress', ->
beforeEach ->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
@request.callsArgWith(1, null, { statusCode: 404 })
it 'remove email', (done)->
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@UserUpdater.updateUser.calledWith(
@@ -95,6 +133,17 @@ describe "UserUpdater", ->
).should.equal true
done()
it 'remove affiliation', (done)->
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@request.calledOnce.should.equal true
requestOptions = @request.lastCall.args[0]
expectedUrl = "v1.url/api/v2/users/#{@stubbedUser._id}/affiliations/"
expectedUrl += encodeURIComponent(@newEmail)
requestOptions.url.should.equal expectedUrl
requestOptions.method.should.equal 'DELETE'
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@@ -103,15 +152,22 @@ describe "UserUpdater", ->
done()
it 'handle missed update', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
it 'handle affiliation error', (done)->
@request.callsArgWith(1, null, { statusCode: 500 })
@UserUpdater.removeEmailAddress @stubbedUser._id, @newEmail, (err)=>
err.message.should.exist
@UserUpdater.updateUser.called.should.equal false
done()
describe 'setDefaultEmailAddress', ->
it 'set default', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 1)
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@@ -129,10 +185,37 @@ describe "UserUpdater", ->
done()
it 'handle missed update', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, nMatched: 0)
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
@UserUpdater.setDefaultEmailAddress @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
describe 'confirmEmail', ->
it 'should update the email record', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 1)
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.not.exist(err)
@UserUpdater.updateUser.calledWith(
{ _id: @stubbedUser._id, 'emails.email': @newEmail },
$set: { 'emails.$.confirmedAt': new Date() }
).should.equal true
done()
it 'handle error', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, new Error('nope'))
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
it 'handle missed update', (done)->
@UserUpdater.updateUser = sinon.stub().callsArgWith(2, null, n: 0)
@UserUpdater.confirmEmail @stubbedUser._id, @newEmail, (err)=>
should.exist(err)
done()
@@ -0,0 +1,66 @@
sinon = require('sinon')
assertCalledWith = sinon.assert.calledWith
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = '../../../../app/js/infrastructure/ProxyManager'
SandboxedModule = require('sandboxed-module')
MockRequest = require "../helpers/MockRequest"
MockResponse = require "../helpers/MockResponse"
describe "ProxyManager", ->
before ->
@settings = proxyUrls: {}
@request = sinon.stub().returns(
on: ()->
pipe: ()->
)
@proxyManager = SandboxedModule.require modulePath, requires:
'settings-sharelatex': @settings
'logger-sharelatex': log: ->
'request': @request
@proxyPath = '/foo/bar'
@req = new MockRequest()
@res = new MockResponse()
@next = sinon.stub()
describe 'proxyUrls', ->
beforeEach ->
@req.url = @proxyPath
@req.query = {}
@settings.proxyUrls = {}
afterEach ->
@next.reset()
@request.reset()
it 'calls next when no match', ->
@proxyManager.call(@req, @res, @next)
sinon.assert.called(@next)
sinon.assert.notCalled(@request)
it 'does not calls next when match', ->
@settings.proxyUrls[@proxyPath] = '/'
@proxyManager.call(@req, @res, @next)
sinon.assert.notCalled(@next)
sinon.assert.called(@request)
it 'proxy full URL', ->
targetUrl = 'https://user:pass@foo.bar:123/pa/th.ext?query#hash'
@settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.call(@req)
assertCalledWith(@request, targetUrl)
it 'overwrite query', ->
targetUrl = 'foo.bar/baz?query'
@req.query = { requestQuery: 'important' }
@settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.call(@req)
newTargetUrl = 'foo.bar/baz?requestQuery=important'
assertCalledWith(@request, newTargetUrl)
it 'handles target objects', ->
targetUrl = { baseUrl: 'api.v1', path: '/pa/th'}
@settings.proxyUrls[@proxyPath] = targetUrl
@proxyManager.call(@req, @res, @next)
assertCalledWith(@request, 'api.v1/pa/th')