ソースを参照

Merge branch 'master' into sk-fully-hide-trackchanges

Shane Kilkelly 9 年 前
親
コミット
05c5b4f9c5

+ 5 - 1
services/web/app/coffee/Features/Authentication/AuthenticationManager.coffee

@@ -29,8 +29,12 @@ module.exports = AuthenticationManager =
 				callback null, null
 
 	setUserPassword: (user_id, password, callback = (error) ->) ->
-		if Settings.passwordStrengthOptions?.length?.max? and Settings.passwordStrengthOptions?.length?.max < password.length
+		if (Settings.passwordStrengthOptions?.length?.max? and
+				Settings.passwordStrengthOptions?.length?.max < password.length)
 			return callback("password is too long")
+		if (Settings.passwordStrengthOptions?.length?.min? and
+				Settings.passwordStrengthOptions?.length?.min > password.length)
+			return callback("password is too short")
 
 		bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
 			return callback(error) if error?

+ 9 - 0
services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee

@@ -62,6 +62,15 @@ module.exports = SubscriptionUpdater =
 			invited_emails: email
 		}, callback
 
+	deleteSubscription: (subscription_id, callback = (error) ->) ->
+		SubscriptionLocator.getSubscription subscription_id, (err, subscription) ->
+			return callback(err) if err?
+			affected_user_ids = [subscription.admin_id].concat(subscription.member_ids or [])
+			logger.log {subscription_id, affected_user_ids}, "deleting subscription and downgrading users"
+			Subscription.remove {_id: ObjectId(subscription_id)}, (err) ->
+				return callback(err) if err?
+				async.mapSeries affected_user_ids, SubscriptionUpdater._setUsersMinimumFeatures, callback
+
 	_createNewSubscription: (adminUser_id, callback)->
 		logger.log adminUser_id:adminUser_id, "creating new subscription"
 		subscription = new Subscription(admin_id:adminUser_id)

+ 2 - 0
services/web/app/coffee/Features/User/UserController.coffee

@@ -74,6 +74,8 @@ module.exports = UserController =
 				user.ace.fontSize = req.body.fontSize
 			if req.body.autoComplete?
 				user.ace.autoComplete = req.body.autoComplete
+			if req.body.autoPairDelimiters?
+				user.ace.autoPairDelimiters = req.body.autoPairDelimiters
 			if req.body.spellCheckLanguage?
 				user.ace.spellCheckLanguage = req.body.spellCheckLanguage
 			if req.body.pdfViewer?

+ 4 - 2
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -11,6 +11,7 @@ async = require("async")
 Modules = require "./Modules"
 Url = require "url"
 PackageVersions = require "./PackageVersions"
+htmlEncoder = new require("node-html-encoder").Encoder("numerical")
 fingerprints = {}
 Path = require 'path'
 
@@ -151,9 +152,10 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 		next()
 
 	webRouter.use (req, res, next)->
-		res.locals.translate = (key, vars = {}) ->
+		res.locals.translate = (key, vars = {}, htmlEncode = false) ->
 			vars.appName = Settings.appName
-			req.i18n.translate(key, vars)
+			str = req.i18n.translate(key, vars)
+			if htmlEncode then htmlEncoder.htmlEncode(str) else str
 		# Don't include the query string parameters, otherwise Google
 		# treats ?nocdn=true as the canonical version
 		res.locals.currentUrl = Url.parse(req.originalUrl).pathname

+ 56 - 31
services/web/app/views/contact-us-modal.pug

@@ -7,36 +7,61 @@ script(type='text/ng-template', id='supportModalTemplate')
 		) &times;
 		h3 #{translate("contact_us")}
 	.modal-body.contact-us-modal
-		span(ng-show="sent == false")
-			label
-				| #{translate("subject")}
-			.form-group
-				input.field.text.medium.span8.form-control(
-					ng-model="form.subject",
-					ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
-					maxlength='255',
-					tabindex='1',
-					onkeyup='')
-			.contact-suggestions(ng-show="suggestions.length")
-				p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })} 
-				ul.contact-suggestion-list
-					li(ng-repeat="suggestion in suggestions")
-						a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
-							span(ng-bind-html="suggestion.name")
-							i.fa.fa-angle-right
-			label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
-				| #{translate("email")}
-			.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
-				input.field.text.medium.span8.form-control(ng-model="form.email",  ng-init="form.email = '"+getUserEmail()+"'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
-			label#title12.desc
-				| #{translate("project_url")} (#{translate("optional")})
-			.form-group
-				input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
-			label.desc
-				| #{translate("contact_message_label")}
-			.form-group
-				textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
-			.form-group.text-center
-				input.btn-success.btn.btn-lg(type='submit', ng-disabled="sending", ng-click="contactUs()" value=translate("contact_us"))
+		form(name="contactForm")
+			span(ng-show="sent == false")
+				.alert.alert-danger(ng-show="error") Something went wrong sending your request :(
+				label
+					| #{translate("subject")}
+				.form-group
+					input.field.text.medium.span8.form-control(
+						name="subject",
+						required
+						ng-model="form.subject",
+						ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
+						maxlength='255',
+						tabindex='1',
+						onkeyup='')
+				.contact-suggestions(ng-show="suggestions.length")
+					p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })} 
+					ul.contact-suggestion-list
+						li(ng-repeat="suggestion in suggestions")
+							a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
+								span(ng-bind-html="suggestion.name")
+								i.fa.fa-angle-right
+				label.desc(ng-show="'"+getUserEmail()+"'.length < 1")
+					| #{translate("email")}
+				.form-group(ng-show="'"+getUserEmail()+"'.length < 1")
+					input.field.text.medium.span8.form-control(
+						name="email",
+						required
+						ng-model="form.email", 
+						ng-init="form.email = '"+getUserEmail()+"'",
+						type='email', spellcheck='false',
+						value='',
+						maxlength='255',
+						tabindex='2')
+				label#title12.desc
+					| #{translate("project_url")} (#{translate("optional")})
+				.form-group
+					input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
+				label.desc
+					| #{translate("contact_message_label")}
+				.form-group
+					textarea.field.text.medium.span8.form-control(
+						name="body",
+						required
+						ng-model="form.message",
+						type='text',
+						value='',
+						tabindex='4',
+						onkeyup=''
+					)
+				.form-group.text-center
+					input.btn-success.btn.btn-lg(
+						type='submit',
+						ng-disabled="contactForm.$invalid || sending",
+						ng-click="contactUs()"
+						value=translate("contact_us")
+					)
 		span(ng-show="sent")
 			p #{translate("request_sent_thank_you")}

+ 1 - 1
services/web/app/views/project/editor/editor.pug

@@ -82,7 +82,7 @@ div.full-size(
 			i.fa.fa-long-arrow-right
 		br
 		a.btn.btn-default.btn-xs(
-			tooltip-html="'"+translate('go_to_pdf_location_in_code')+"'"
+			tooltip-html="'"+translate('go_to_pdf_location_in_code', {}, true)+"'"
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			ng-click="syncToCode()"

+ 2 - 2
services/web/config/settings.defaults.coffee

@@ -226,8 +226,8 @@ module.exports = settings =
 	# passwordStrengthOptions:
 	# 	pattern: "aA$3"
 	# 	length:
-	# 		min: 1
-	# 		max: 10
+	# 		min: 6
+	# 		max: 128
 
 	# Email support
 	# -------------

ファイルの差分が大きいため隠しています
+ 753 - 84
services/web/npm-shrinkwrap.json


+ 8 - 7
services/web/package.json

@@ -30,8 +30,8 @@
     "ioredis": "^2.4.0",
     "jade": "~1.3.1",
     "ldapjs": "^0.7.1",
-    "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
     "lodash": "^4.13.1",
+    "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
     "lynx": "0.1.1",
     "marked": "^0.3.5",
     "method-override": "^2.3.3",
@@ -39,8 +39,9 @@
     "mimelib": "0.2.14",
     "mocha": "1.17.1",
     "mongojs": "2.4.0",
-    "mongoose": "4.1.0",
+    "mongoose": "4.11.4",
     "multer": "^0.1.8",
+    "node-html-encoder": "0.0.2",
     "nodemailer": "2.1.0",
     "nodemailer-sendgrid-transport": "^0.2.0",
     "nodemailer-ses-transport": "^1.3.0",
@@ -48,23 +49,23 @@
     "passport": "^0.3.2",
     "passport-ldapauth": "^0.6.0",
     "passport-local": "^1.0.0",
+    "passport-saml": "^0.15.0",
+    "pug": "^2.0.0-beta6",
     "redis": "0.10.1",
     "redis-sharelatex": "git+https://github.com/sharelatex/redis-sharelatex.git#v1.0.2",
     "request": "^2.69.0",
     "requests": "^0.1.7",
     "rimraf": "2.2.6",
+    "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master",
     "sanitizer": "0.1.1",
     "sequelize": "^3.2.0",
     "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
     "sixpack-client": "^1.0.0",
     "temp": "^0.8.3",
     "underscore": "1.6.0",
-    "v8-profiler": "^5.2.3",
-    "xml2js": "0.2.0",
-    "passport-saml": "^0.15.0",
-    "pug": "^2.0.0-beta6",
     "uuid": "^3.0.1",
-    "rolling-rate-limiter": "git+https://github.com/ShaneKilkelly/rolling-rate-limiter.git#master"
+    "v8-profiler": "^5.2.3",
+    "xml2js": "0.2.0"
   },
   "devDependencies": {
     "autoprefixer": "^6.6.1",

+ 5 - 4
services/web/public/coffee/directives/asyncForm.coffee

@@ -103,8 +103,8 @@ define [
 			defaultPasswordOpts =
 				pattern: ""
 				length:
-					min: 1
-					max: 50
+					min: 6
+					max: 128
 				allowEmpty: false
 				allowAnyChars: false
 				isMasked: true
@@ -127,8 +127,6 @@ define [
 			[asyncFormCtrl, ngModelCtrl] = ctrl
 
 			ngModelCtrl.$parsers.unshift (modelValue) ->
-				
-			
 				isValid = passField.validatePass()
 				email = asyncFormCtrl.getEmail() || window.usersEmail
 				if !isValid
@@ -141,5 +139,8 @@ define [
 				if opts.length.max? and modelValue.length == opts.length.max
 					isValid = false
 					scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached"
+				if opts.length.min? and modelValue.length < opts.length.min
+					isValid = false
+					scope.complexPasswordErrorMessage = "Password too short, minimum #{opts.length.min}"
 				ngModelCtrl.$setValidity('complexPassword', isValid)
 				return modelValue

+ 4 - 3
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -11,6 +11,8 @@ define [
 	"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
 	"ide/editor/directives/aceEditor/labels/LabelsManager"
 	"ide/labels/services/labels"
+	"ide/graphics/services/graphics"
+	"ide/preamble/services/preamble"
 ], (App, Ace, SearchBox, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, LabelsManager) ->
 	EditSession = ace.require('ace/edit_session').EditSession
 	ModeList = ace.require('ace/ext/modelist')
@@ -33,9 +35,8 @@ define [
 			url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}"
 			return url
 
-	App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels) ->
+	App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, labels, graphics, preamble) ->
 		monkeyPatchSearch($rootScope, $compile)
-		
 
 		return  {
 			scope: {
@@ -102,7 +103,7 @@ define [
 				cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage)
 				trackChangesManager   = new TrackChangesManager(scope, editor, element)
 				labelsManager = new LabelsManager(scope, editor, element, labels)
-				autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager)
+				autoCompleteManager = new AutoCompleteManager(scope, editor, element, labelsManager, graphics, preamble)
 
 
 				# Prevert Ctrl|Cmd-S from triggering save dialog

+ 55 - 3
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -17,7 +17,7 @@ define [
 		commandFragment?.match(/\\(\w+)\{/)?[1]
 
 	class AutoCompleteManager
-		constructor: (@$scope, @editor, @element, @labelsManager) ->
+		constructor: (@$scope, @editor, @element, @labelsManager, @graphics, @preamble) ->
 			@suggestionManager = new SuggestionManager()
 
 			@monkeyPatchAutocomplete()
@@ -44,6 +44,37 @@ define [
 
 			SnippetCompleter = new SnippetManager()
 
+			Graphics = @graphics
+			Preamble = @preamble
+			GraphicsCompleter =
+				getCompletions: (editor, session, pos, prefix, callback) ->
+					upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
+					lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
+					commandFragment = getLastCommandFragment(lineUpToCursor)
+					if commandFragment
+						match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/)
+						if match
+							beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
+							lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
+							needsClosingBrace = !lineBeyondCursor.match(/^[^{]*}/)
+							commandName = match[1]
+							currentArg = match[3]
+							graphicsPaths = Preamble.getGraphicsPaths()
+							result = []
+							for graphic in Graphics.getGraphicsFiles()
+								path = graphic.path
+								for graphicsPath in graphicsPaths
+									if path.indexOf(graphicsPath) == 0
+										path = path.slice(graphicsPath.length)
+										break
+								result.push {
+									caption: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
+									value: "\\#{commandName}{#{path}#{if needsClosingBrace then '}' else ''}",
+									meta: "graphic",
+									score: 50
+								}
+							callback null, result
+
 			labelsManager = @labelsManager
 			LabelsCompleter =
 				getCompletions: (editor, session, pos, prefix, callback) ->
@@ -112,7 +143,13 @@ define [
 							else
 								callback null, result
 
-			@editor.completers = [@suggestionManager, SnippetCompleter, ReferencesCompleter, LabelsCompleter]
+			@editor.completers = [
+				@suggestionManager,
+				SnippetCompleter,
+				ReferencesCompleter,
+				LabelsCompleter,
+				GraphicsCompleter
+			]
 
 		disable: () ->
 			@editor.setOptions({
@@ -245,7 +282,22 @@ define [
 						editor.completer.autoSelect = true
 						editor.completer.showPopup(editor)
 						editor.completer.cancelContextMenu()
-						$(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'})
+						container = $(editor.completer.popup?.container)
+						container.css({'font-size': @$scope.fontSize + 'px'})
+						# Dynamically set width of autocomplete popup
+						if filtered = editor?.completer?.completions?.filtered
+							longestCaption = _.max(filtered.map( (c) -> c.caption.length ))
+							longestMeta = _.max(filtered.map( (c) -> c.meta.length ))
+							charWidth = editor.renderer.characterWidth
+							# between 280 and 700 px
+							width = Math.max(
+								Math.min(
+									Math.round(longestCaption*charWidth + longestMeta*charWidth + 5*charWidth),
+									700
+								),
+								280
+							)
+							container.css({width: "#{width}px"})
 						if editor.completer?.completions?.filtered?.length == 0
 							editor.completer.detach()
 					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"

+ 17 - 0
services/web/public/coffee/ide/graphics/services/graphics.coffee

@@ -0,0 +1,17 @@
+define [
+	"base"
+], (App) ->
+
+	App.factory 'graphics', (ide) ->
+
+		Graphics =
+			getGraphicsFiles: () ->
+				graphicsFiles = []
+				ide.fileTreeManager.forEachEntity (entity, folder, path) ->
+					if entity.type == 'file' && entity?.name?.match?(/.*\.(png|jpg|jpeg|pdf|eps)/)
+						cloned = _.clone(entity)
+						cloned.path = path
+						graphicsFiles.push cloned
+				return graphicsFiles
+
+		return Graphics

+ 22 - 0
services/web/public/coffee/ide/preamble/services/preamble.coffee

@@ -0,0 +1,22 @@
+define [
+	"base"
+], (App) ->
+
+	App.factory 'preamble', (ide) ->
+
+		Preamble =
+			getPreambleText: () ->
+				text = ide.editorManager.getCurrentDocValue().slice(0, 5000)
+				preamble = text.match(/([^]*)^\\begin\{document\}/m)?[1] || ""
+				return preamble
+
+			getGraphicsPaths: () ->
+				preamble = Preamble.getPreambleText()
+				graphicsPathsArgs = preamble.match(/\\graphicspath\{(.*)\}/)?[1] || ""
+				paths = []
+				re = /\{([^}]*)\}/g
+				while match = re.exec(graphicsPathsArgs)
+					paths.push(match[1])
+				return paths
+
+		return Preamble

+ 11 - 3
services/web/public/coffee/main/contact-us.coffee

@@ -30,7 +30,7 @@ define [
 				$scope.suggestions = suggestions
 
 		$scope.contactUs = ->
-			if !$scope.form.email?
+			if !$scope.form.email? or $scope.form.email == ""
 				console.log "email not set"
 				return
 			$scope.sending = true
@@ -46,8 +46,16 @@ define [
 				about: "<div>browser: #{platform?.name} #{platform?.version}</div>
 						<div>os: #{platform?.os?.family} #{platform?.os?.version}</div>"
 
-			Groove.createTicket params, (err, json)->
-				$scope.sent = true
+			Groove.createTicket params, (response)->
+				$scope.sending = false
+				if response.responseText == "" # Blocked request or similar
+					$scope.error = true
+				else
+					data = JSON.parse(response.responseText)
+					if data.errors?
+						$scope.error = true
+					else
+						$scope.sent = true
 				$scope.$apply()
 
 		$scope.$watch "form.subject", (newVal, oldVal) ->

+ 1 - 1
services/web/public/coffee/main/project-list/project-list.coffee

@@ -20,7 +20,7 @@ define [
 		, 10
 
 		$scope.$watch((
-			() -> $scope.projects.filter((project) -> !project.tags? or project.tags.length == 0).length
+			() -> $scope.projects.filter((project) -> (!project.tags? or project.tags.length == 0) and !project.archived).length
 		), (newVal) -> $scope.nUntagged = newVal)
 
 		storedUIOpts = JSON.parse(localStorage("project_list"))

+ 1 - 0
services/web/public/stylesheets/app/editor.less

@@ -504,3 +504,4 @@
 	height: auto;
 	border-bottom: 1px solid @modal-header-border-color;
 }
+

+ 4 - 0
services/web/public/stylesheets/components/navs.less

@@ -225,6 +225,10 @@
 
 // Hide tabbable panes to start, show them when `.active`
 .tab-content {
+  background-color: @nav-tabs-active-link-hover-bg;
+  border: 1px solid @nav-tabs-border-color;
+  border-top: none;
+  padding: @line-height-computed / 2;
   > .tab-pane {
     display: none;
   }

+ 18 - 0
services/web/test/UnitTests/coffee/Authentication/AuthenticationManagerTests.coffee

@@ -116,6 +116,24 @@ describe "AuthenticationManager", ->
 					expect(err).to.exist
 					done()
 
+			it "should not start the bcrypt process", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>
+					@bcrypt.genSalt.called.should.equal false
+					@bcrypt.hash.called.should.equal false
+					done()
+
+		describe "too short", ->
+			beforeEach ->
+				@settings.passwordStrengthOptions =
+					length:
+						max:10
+						min:6
+				@password = "dsd"
+
+			it "should return and error", (done)->
+				@AuthenticationManager.setUserPassword @user_id, @password, (err)->
+					expect(err).to.exist
+					done()
 
 			it "should not start the bcrypt process", (done)->
 				@AuthenticationManager.setUserPassword @user_id, @password, (err)=>

+ 33 - 0
services/web/test/UnitTests/coffee/Subscription/SubscriptionUpdaterTests.coffee

@@ -37,6 +37,7 @@ describe "SubscriptionUpdater", ->
 			constructor: (opts)-> 
 				subscription.admin_id = opts.admin_id
 				return subscription
+			@remove: sinon.stub().yields()
 		@SubscriptionModel.update = @updateStub
 		@SubscriptionModel.findAndModify = @findAndModifyStub
 
@@ -230,3 +231,35 @@ describe "SubscriptionUpdater", ->
 				@ReferalAllocator.assignBonus.calledWith(@adminuser_id).should.equal true
 				done()
 
+	describe "deleteSubscription", ->
+		beforeEach (done) ->
+			@subscription_id = ObjectId().toString()
+			@subscription = {
+				"mock": "subscription",
+				admin_id: ObjectId(),
+				member_ids: [ ObjectId(), ObjectId(), ObjectId() ]
+			}
+			@SubscriptionLocator.getSubscription = sinon.stub().yields(null, @subscription)
+			@SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub().yields()
+			@SubscriptionUpdater.deleteSubscription @subscription_id, done
+			
+		it "should look up the subscription", ->
+			@SubscriptionLocator.getSubscription
+				.calledWith(@subscription_id)
+				.should.equal true
+		
+		it "should remove the subscription", ->
+			@SubscriptionModel.remove
+				.calledWith({_id: ObjectId(@subscription_id)})
+				.should.equal true
+		
+		it "should downgrade the admin_id", ->
+			@SubscriptionUpdater._setUsersMinimumFeatures
+				.calledWith(@subscription.admin_id)
+				.should.equal true
+		
+		it "should downgrade all of the members", ->
+			for user_id in @subscription.member_ids
+				@SubscriptionUpdater._setUsersMinimumFeatures
+					.calledWith(user_id)
+					.should.equal true

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません