Merge branch 'master' into pr-user-affilitations
This commit is contained in:
+11
-6
@@ -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;"> </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;"> </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 } – shared by #{ opts.owner.email }"
|
||||
greeting: "Hi,"
|
||||
message: "#{ opts.owner.email } wants to share “#{ opts.project.name }” 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
|
||||
|
||||
@@ -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 : ''}
|
||||
|
||||
@@ -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")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-select"
|
||||
ng-model="data.selectedProjectId"
|
||||
ng-disabled="!shouldEnableProjectSelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a Project
|
||||
option(
|
||||
ng-repeat="project in data.projects"
|
||||
value="{{ project._id }}"
|
||||
) {{ project.name }}
|
||||
|
||||
br
|
||||
.form-controls
|
||||
label(for="project-entity-select") Select a File
|
||||
span(ng-show="state.inFlight.entities")
|
||||
|
|
||||
i.fa.fa-spinner.fa-spin
|
||||
select.form-control(
|
||||
name="project-entity-select"
|
||||
ng-model="data.selectedProjectEntity"
|
||||
ng-disabled="!shouldEnableProjectEntitySelect()"
|
||||
)
|
||||
option(value="" disabled selected) - Please Select a File
|
||||
option(
|
||||
ng-repeat="projectEntity in data.projectEntities"
|
||||
value="{{ projectEntity.path }}"
|
||||
) {{ projectEntity.path.slice(1) }}
|
||||
br
|
||||
|
||||
.form-controls
|
||||
label(for="name") File Name In This Project
|
||||
input.form-control(
|
||||
type="text"
|
||||
placeholder="example.tex"
|
||||
required
|
||||
ng-model="data.name"
|
||||
name="name"
|
||||
)
|
||||
br
|
||||
|
||||
.modal-footer
|
||||
span(ng-show="state.inFlight.create")
|
||||
i.fa.fa-spinner.fa-spin
|
||||
|
|
||||
button.btn.btn-default(
|
||||
ng-disabled="state.inflight"
|
||||
ng-click="cancel()"
|
||||
) #{translate("cancel")}
|
||||
button.btn.btn-primary(
|
||||
ng-disabled="!shouldEnableCreateButton()"
|
||||
ng-click="create()"
|
||||
)
|
||||
span(ng-hide="state.inflight") #{translate("create")}
|
||||
span(ng-show="state.inflight") #{translate("creating")}...
|
||||
|
||||
|
||||
script(type='text/ng-template', id='linkedFileModalTemplate')
|
||||
.modal-header
|
||||
h3 New file from URL
|
||||
.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
|
||||
|
|
||||
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
|
||||
|
|
||||
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")
|
||||
|
|
||||
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")
|
||||
|
|
||||
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")
|
||||
|
|
||||
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
|
||||
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'")
|
||||
|
||||
@@ -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 & 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
|
||||
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"))
|
||||
|
|
||||
span.img-circle
|
||||
img(src=buildImgPath("about/james_allen.jpg"))
|
||||
| The #{settings.appName} Team
|
||||
p
|
||||
a.btn.btn-primary(href="/project") < #{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...
|
||||
@@ -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", () ->
|
||||
|
||||
+30
-31
@@ -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.
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
-4
@@ -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
|
||||
|
||||
+1
-4
@@ -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
|
||||
|
||||
+3
-6
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -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')
|
||||
Reference in New Issue
Block a user