Merge branch 'master' into sk-session-revocation

This commit is contained in:
Shane Kilkelly 2016-07-06 13:19:15 +01:00
commit fc6cf75ad5
27 changed files with 244 additions and 76 deletions

View file

@ -17,8 +17,9 @@ module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-contrib-watch'
grunt.loadNpmTasks 'grunt-parallel'
grunt.loadNpmTasks 'grunt-exec'
grunt.loadNpmTasks 'grunt-contrib-imagemin'
grunt.loadNpmTasks 'grunt-contrib-cssmin'
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
# grunt.loadNpmTasks 'grunt-sprity'
config =
@ -47,18 +48,26 @@ module.exports = (grunt) ->
stream:true
imagemin:
dynamic:
files: [{
expand: true
cwd: 'public/img/'
src: ['**/*.{png,jpg,gif}']
dest: 'public/img/'
}]
options:
interlaced:false
optimizationLevel: 7
# imagemin:
# dynamic:
# files: [{
# expand: true
# cwd: 'public/img/'
# src: ['**/*.{png,jpg,gif}']
# dest: 'public/img/'
# }]
# options:
# interlaced:false
# optimizationLevel: 7
# sprity:
# sprite:
# options:
# cssPath:"/img/"
# 'style': '../../public/stylesheets/app/sprites.less'
# margin: 0
# src: ['./public/img/flags/24/*.png']
# dest: './public/img/sprite'
coffee:
@ -220,6 +229,7 @@ module.exports = (grunt) ->
availabletasks:
tasks:
options:

View file

@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo
It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains
a lot of logic around creating and editing projects, and account management.
The rest of the ShareLaTeX stack, along with information about contributing can be found in the
[sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
Build process
----------------
web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets.
Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
Unit test status
----------------

View file

@ -20,7 +20,7 @@ module.exports = BlogController =
logger.log url:url, "proxying request to blog api"
request.get blogUrl, (err, r, data)->
if r?.statusCode == 404
if r?.statusCode == 404 or r?.statusCode == 403
return ErrorController.notFound(req, res, next)
if err?
return res.send 500

View file

@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
return callback(error) if error?
result = []
async.mapLimit members, 3,
(member, cb) ->
UserGetter.getUser member.id, (error, user) ->
return cb(error) if error?
return cb(null, { user: user, privilegeLevel: member.privilegeLevel })
callback
if user?
result.push { user: user, privilegeLevel: member.privilegeLevel }
cb()
(error) ->
return callback(error) if error?
callback null, result
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
@ -82,6 +87,18 @@ module.exports = CollaboratorsHandler =
logger.error err: err, "problem removing user from project collaberators"
callback(err)
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
return callback(error) if error?
allProjects = readAndWriteProjects.concat(readOnlyProjects)
jobs = []
for project in allProjects
do (project) ->
jobs.push (cb) ->
return cb() if !project?
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
async.series jobs, callback
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
emails = mimelib.parseAddresses(unparsed_email)
email = emails[0]?.address?.toLowerCase()

View file

@ -29,8 +29,6 @@ module.exports = CompileController =
options.compiler = req.body.compiler
if req.body?.draft
options.draft = req.body.draft
if req.query?.isolated is "true"
options.isolated = true
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
return next(error) if error?
@ -44,17 +42,15 @@ module.exports = CompileController =
}
_compileAsUser: (req, callback) ->
# callback with user_id if isolated flag is set on request, undefined otherwise
isolated = req.query?.isolated is "true"
if isolated
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
else
callback() # do a per-project compile, not per-user
_downloadAsUser: (req, callback) ->
# callback with user_id if isolated flag or user_id param is set on request, undefined otherwise
isolated = req.query?.isolated is "true" or req.params.user_id?
if isolated
# callback with user_id if per-user, undefined otherwise
if not Settings.disablePerUserCompiles
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
else
callback() # do a per-project compile, not per-user

View file

@ -38,7 +38,7 @@ module.exports = CompileManager =
for key, value of limits
options[key] = value
# only pass user_id down to clsi if this is a per-user compile
compileAsUser = if options.isolated then user_id else undefined
compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
return callback(error) if error?
logger.log files: outputFiles, "output files"

View file

@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
settings = require("settings-sharelatex")
templates = {}
templates.registered =
@ -114,6 +116,8 @@ module.exports =
template = templates[templateName]
opts.siteUrl = settings.siteUrl
opts.body = template.compiledTemplate(opts)
if settings.email?.templates?.customFooter?
opts.body += settings.email?.templates?.customFooter
return {
subject : template.subject(opts)
html: template.layout(opts)

View file

@ -6,7 +6,7 @@ oneSecond = 1000
makeRequest = (opts, callback)->
if !settings.apis.notifications?.url?
return callback()
return callback(null, statusCode:200)
else
request(opts, callback)
@ -18,7 +18,7 @@ module.exports =
json: true
timeout: oneSecond
method: "GET"
request opts, (err, res, unreadNotifications)->
makeRequest opts, (err, res, unreadNotifications)->
statusCode = if res? then res.statusCode else 500
if err? or statusCode != 200
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
@ -40,7 +40,7 @@ module.exports =
templateKey:templateKey
}
logger.log opts:opts, "creating notification for user"
request opts, callback
makeRequest opts, callback
markAsReadWithKey: (user_id, key, callback)->
opts =
@ -51,7 +51,7 @@ module.exports =
key:key
}
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
request opts, callback
makeRequest opts, callback
markAsRead: (user_id, notification_id, callback)->
@ -60,4 +60,4 @@ module.exports =
uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
timeout:oneSecond
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
request opts, callback
makeRequest opts, callback

View file

@ -24,9 +24,11 @@ module.exports = ProjectDeleter =
update = {deletedByExternalDataSource: false}
Project.update conditions, update, {}, callback
deleteUsersProjects: (owner_id, callback)->
logger.log owner_id:owner_id, "deleting users projects"
Project.remove owner_ref:owner_id, callback
deleteUsersProjects: (user_id, callback)->
logger.log {user_id}, "deleting users projects"
Project.remove owner_ref:user_id, (error) ->
return callback(error) if error?
CollaboratorsHandler.removeUserFromAllProjets user_id, callback
deleteProject: (project_id, callback = (error) ->) ->
# archiveProject takes care of the clean-up

View file

@ -15,7 +15,7 @@ footer.site-footer
aria-expanded="false",
tooltip="#{translate('language')}"
)
img(src="/img/flags/24/#{currentLngCode}.png")
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
ul.dropdown-menu(role="menu")
li.dropdown-header #{translate("language")}
@ -23,9 +23,9 @@ footer.site-footer
if !subdomainDetails.hide
li.lngOption
a.menu-indent(href=subdomainDetails.url+currentUrl)
img(src="/img/flags/24/#{subdomainDetails.lngCode}.png")
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
| #{translate(subdomainDetails.lngCode)}
//- img(src="/img/flags/24/.png")
each item in nav.left_footer
li
if item.url

View file

@ -128,12 +128,12 @@ div.full-size.pdf(ng-controller="PdfController")
)
label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
a.card-hint-feedback-positive(
ng-click="feedbackSent = true;"
ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;"
href
) #{translate("answer_yes")}
span  / 
a.card-hint-feedback-negative(
ng-click="feedbackSent = true;"
ng-click="trackLogHintsNegativeFeedback(entry.ruleId); feedbackSent = true;"
href
) #{translate("answer_no")}
.card-hint-feedback(ng-show="feedbackSent")

View file

@ -263,6 +263,10 @@ module.exports = settings =
# public projects, /learn, /templates, about pages, etc.
allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
# Use a single compile directory for all users in a project
# (otherwise each user has their own directory)
# disablePerUserCompiles: true
# Maximum size of text documents in the real-time editing system.
max_doc_length: 2 * 1024 * 1024 # 2mb

View file

@ -70,7 +70,6 @@
"grunt-contrib-clean": "0.5.0",
"grunt-contrib-coffee": "0.10.0",
"grunt-contrib-cssmin": "^1.0.1",
"grunt-contrib-imagemin": "^1.0.1",
"grunt-contrib-less": "0.9.0",
"grunt-contrib-requirejs": "0.4.1",
"grunt-contrib-watch": "^1.0.0",

View file

@ -12,7 +12,8 @@ define [
ruleDetails = _getRule entry.message
if (ruleDetails?)
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() if ruleDetails.regexToMatch?
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?

View file

@ -15,6 +15,13 @@ define [
$scope.shouldShowLogs = false
$scope.wikiEnabled = window.wikiEnabled;
# log hints tracking
trackLogHintsFeedback = (isPositive, hintId) ->
event_tracking.send 'log-hints', (if isPositive then 'feedback-positive' else 'feedback-negative'), hintId
$scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId
$scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId
if ace.require("ace/lib/useragent").isMac
$scope.modifierKey = "Cmd"
else
@ -50,8 +57,6 @@ define [
params = {}
if options.isAutoCompile
params["auto_compile"]=true
if perUserCompile # send ?isolated=true for per-user compiles
params["isolated"] = true
return $http.post url, {
rootDoc_id: options.rootDocOverride_id or null
draft: $scope.draft
@ -125,9 +130,6 @@ define [
# convert the qs hash into a query string and append it
$scope.pdf.qs = createQueryString qs
$scope.pdf.url += $scope.pdf.qs
# special case for the download url
if perUserCompile
qs.isolated = true
# Save all downloads as files
qs.popupDownload = true
$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
@ -147,8 +149,6 @@ define [
else
file.name = file.path
qs = {}
if perUserCompile
qs.isolated = true
if response.clsiServerId?
qs.clsiserverid = response.clsiServerId
file.url = "/project/#{project_id}/output/#{file.path}" + createQueryString qs
@ -237,7 +237,7 @@ define [
return null
normalizeFilePath = (path) ->
path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "")
path = path.replace(/^\/compile\//, "")
rootDocDirname = ide.fileTreeManager.getRootDocDirname()
@ -274,7 +274,6 @@ define [
method: "DELETE"
params:
clsiserverid:ide.clsiServerId
isolated: perUserCompile
headers:
"X-Csrf-Token": window.csrfToken
}
@ -361,7 +360,6 @@ define [
line: row + 1
column: column
clsiserverid:ide.clsiServerId
isolated: perUserCompile
}
})
.success (data) ->
@ -407,7 +405,6 @@ define [
h: h.toFixed(2)
v: v.toFixed(2)
clsiserverid:ide.clsiServerId
isolated: perUserCompile
}
})
.success (data) ->

View file

@ -12,7 +12,8 @@ define [
constructor: (@url, @options) ->
# PDFJS.disableFontFace = true # avoids repaints, uses worker more
# PDFJS.disableAutoFetch = true # enable this to prevent loading whole file
if @options.disableAutoFetch
PDFJS.disableAutoFetch = true # prevent loading whole file
# PDFJS.disableStream
# PDFJS.disableRange
@scale = @options.scale || 1

View file

@ -27,13 +27,21 @@ define [
$scope.document.destroy() if $scope.document?
$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
# TODO need a proper url manipulation library to add to query string
$scope.document = new PDFRenderer($scope.pdfSrc + '&pdfng=true' , {
url = $scope.pdfSrc
# add 'pdfng=true' to show that we are using the angular pdfjs viewer
queryStringExists = url.match(/\?/)
url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true'
# for isolated compiles, load the pdf on-demand because nobody will overwrite it
onDemandLoading = window.location?.search?.match(/isolated=true/)?
$scope.document = new PDFRenderer(url, {
scale: 1,
disableAutoFetch: if onDemandLoading then true else undefined
navigateFn: (ref) ->
# this function captures clicks on the annotation links
$scope.navigateTo = ref
$scope.$apply()
progressCallback: (progress) ->
return if onDemandLoading is true # don't show progress for on-demand page loading
$scope.$emit 'progress', progress
loadedCallback: () ->
$scope.$emit 'loaded'

View file

@ -5,15 +5,11 @@ define [
$scope.status =
loading:true
# enable per-user containers by default
perUserCompile = true
opts =
url:"/project/#{ide.project_id}/wordcount"
method:"GET"
params:
clsiserverid:ide.clsiServerId
isolated: perUserCompile
$http opts
.success (data) ->
$scope.status.loading = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,105 @@
.sprite-icon {
background-image: url('/img/sprite.png');
}
.sprite-icon-ko {
background-position: -0px -0px;
width: 24px;
height: 24px;
}
.sprite-icon-cn {
background-position: -0px -24px;
width: 24px;
height: 24px;
}
.sprite-icon-da {
background-position: -0px -48px;
width: 24px;
height: 24px;
}
.sprite-icon-de {
background-position: -0px -72px;
width: 24px;
height: 24px;
}
.sprite-icon-en {
background-position: -0px -96px;
width: 24px;
height: 24px;
}
.sprite-icon-es {
background-position: -0px -120px;
width: 24px;
height: 24px;
}
.sprite-icon-fi {
background-position: -0px -144px;
width: 24px;
height: 24px;
}
.sprite-icon-fr {
background-position: -0px -168px;
width: 24px;
height: 24px;
}
.sprite-icon-it {
background-position: -0px -192px;
width: 24px;
height: 24px;
}
.sprite-icon-ja {
background-position: -0px -216px;
width: 24px;
height: 24px;
}
.sprite-icon-cs {
background-position: -0px -240px;
width: 24px;
height: 24px;
}
.sprite-icon-nl {
background-position: -0px -264px;
width: 24px;
height: 24px;
}
.sprite-icon-no {
background-position: -0px -288px;
width: 24px;
height: 24px;
}
.sprite-icon-pl {
background-position: -0px -312px;
width: 24px;
height: 24px;
}
.sprite-icon-pt {
background-position: -0px -336px;
width: 24px;
height: 24px;
}
.sprite-icon-ru {
background-position: -0px -360px;
width: 24px;
height: 24px;
}
.sprite-icon-sv {
background-position: -0px -384px;
width: 24px;
height: 24px;
}
.sprite-icon-tr {
background-position: -0px -408px;
width: 24px;
height: 24px;
}
.sprite-icon-uk {
background-position: -0px -432px;
width: 24px;
height: 24px;
}
.sprite-icon-zh-CN {
background-position: -0px -456px;
width: 24px;
height: 24px;
}

View file

@ -41,11 +41,6 @@
.example {
max-width: 100%;
& > div {
display: block !important;
width: auto !important;
}
.code {
pre {
background-color: @gray-lightest;
@ -60,9 +55,9 @@
padding-top: 10px;
img {
width: auto !important;
height: auto !important;
max-width: 100% !important;
width: auto;
height: auto;
max-width: 100%;
box-shadow: 0 1px 3px @gray-light;
border-radius: 6px;
}

View file

@ -23,3 +23,8 @@ footer.site-footer {
}
}
}
.sprite-icon-lang {
display: inline-block;
vertical-align: middle;
}

View file

@ -73,3 +73,4 @@
@import "app/wiki.less";
@import "app/translations.less";
@import "app/contact-us.less";
@import "app/sprites.less";

View file

@ -77,17 +77,19 @@ describe "CollaboratorsHandler", ->
{ id: "read-only-ref-2", privilegeLevel: "readOnly" }
{ id: "read-write-ref-1", privilegeLevel: "readAndWrite" }
{ id: "read-write-ref-2", privilegeLevel: "readAndWrite" }
{ id: "doesnt-exist", privilegeLevel: "readAndWrite" }
])
@UserGetter.getUser = sinon.stub()
@UserGetter.getUser.withArgs("read-only-ref-1").yields(null, { _id: "read-only-ref-1" })
@UserGetter.getUser.withArgs("read-only-ref-2").yields(null, { _id: "read-only-ref-2" })
@UserGetter.getUser.withArgs("read-write-ref-1").yields(null, { _id: "read-write-ref-1" })
@UserGetter.getUser.withArgs("read-write-ref-2").yields(null, { _id: "read-write-ref-2" })
@UserGetter.getUser.withArgs("doesnt-exist").yields(null, null)
@CollaboratorHandler.getMembersWithPrivilegeLevels @project_id, @callback
it "should return an array of members with their privilege levels", ->
@callback
.calledWith(undefined, [
.calledWith(null, [
{ user: { _id: "read-only-ref-1" }, privilegeLevel: "readOnly" }
{ user: { _id: "read-only-ref-2" }, privilegeLevel: "readOnly" }
{ user: { _id: "read-write-ref-1" }, privilegeLevel: "readAndWrite" }
@ -274,6 +276,19 @@ describe "CollaboratorsHandler", ->
it "should not add any users to the proejct", ->
@CollaboratorHandler.addUserIdToProject.called.should.equal false
describe "removeUserFromAllProjects", ->
beforeEach (done) ->
@CollaboratorHandler.getProjectsUserIsCollaboratorOf = sinon.stub()
@CollaboratorHandler.getProjectsUserIsCollaboratorOf.withArgs(@user_id, { _id: 1 }).yields(
null,
[ { _id: "read-and-write-0" }, { _id: "read-and-write-1" }, null ],
[ { _id: "read-only-0" }, { _id: "read-only-1" }, null ]
)
@CollaboratorHandler.removeUserFromProject = sinon.stub().yields()
@CollaboratorHandler.removeUserFromAllProjets @user_id, done
it "should remove the user from each project", ->
for project_id in ["read-and-write-0", "read-and-write-1", "read-only-0", "read-only-1"]
@CollaboratorHandler.removeUserFromProject
.calledWith(project_id, @user_id)
.should.equal true

View file

@ -139,7 +139,7 @@ describe "CompileController", ->
.should.equal true
it "should proxy the PDF from the CLSI", ->
@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/output/output.pdf", @req, @res, @next).should.equal true
@CompileController.proxyToClsi.calledWith(@project_id, "/project/#{@project_id}/user/#{@user_id}/output/output.pdf", @req, @res, @next).should.equal true
describe "when the pdf is not going to be used in pdfjs viewer", ->
@ -338,8 +338,6 @@ describe "CompileController", ->
@req =
params:
project_id:@project_id
query:
isolated: "true"
@CompileManager.compile.callsArgWith(3)
@CompileController.proxyToClsi = sinon.stub()
@res =
@ -362,8 +360,6 @@ describe "CompileController", ->
@CompileManager.wordCount = sinon.stub().callsArgWith(3, null, {content:"body"})
@req.params =
Project_id: @project_id
@req.query =
isolated: "true"
@res.send = sinon.stub()
@res.contentType = sinon.stub()
@CompileController.wordCount @req, @res, @next

View file

@ -71,7 +71,7 @@ describe "CompileManager", ->
it "should run the compile with the compile limits", ->
@ClsiManager.sendRequest
.calledWith(@project_id, undefined, {
.calledWith(@project_id, @user_id, {
timeout: @limits.timeout
})
.should.equal true

View file

@ -27,13 +27,15 @@ describe 'ProjectDeleter', ->
removeProjectFromAllTags: sinon.stub().callsArgWith(2)
@ProjectGetter =
getProject:sinon.stub()
@CollaboratorsHandler =
removeUserFromAllProjets: sinon.stub().yields()
@deleter = SandboxedModule.require modulePath, requires:
"../Editor/EditorController": @editorController
'../../models/Project':{Project:@Project}
'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
"../Tags/TagsHandler":@TagsHandler
"../FileStore/FileStoreHandler": @FileStoreHandler = {}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler = {}
"../Collaborators/CollaboratorsHandler": @CollaboratorsHandler
"./ProjectGetter": @ProjectGetter
'logger-sharelatex':
log:->
@ -74,6 +76,12 @@ describe 'ProjectDeleter', ->
@Project.remove.calledWith(owner_ref:user_id).should.equal true
done()
it "should remove all the projects the user is a collaborator of", (done)->
user_id = 1234
@deleter.deleteUsersProjects user_id, =>
@CollaboratorsHandler.removeUserFromAllProjets.calledWith(user_id).should.equal true
done()
describe "deleteProject", ->
beforeEach (done) ->
@project_id = "mock-project-id-123"