Просмотр исходного кода

Merge branch 'master' into as-v1-dash-link

Alasdair Smith 8 лет назад
Родитель
Сommit
4126e9bd31
31 измененных файлов с 597 добавлено и 701 удалено
  1. 21 0
      services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee
  2. 2 1
      services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee
  3. 2 0
      services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee
  4. 6 5
      services/web/app/coffee/Features/Email/EmailSender.coffee
  5. 0 28
      services/web/app/coffee/Features/Labels/LabelsController.coffee
  6. 0 43
      services/web/app/coffee/Features/Labels/LabelsHandler.coffee
  7. 11 0
      services/web/app/coffee/infrastructure/ExpressLocals.coffee
  8. 0 4
      services/web/app/coffee/router.coffee
  9. 9 0
      services/web/app/views/layout.pug
  10. 3 2
      services/web/app/views/project/editor.pug
  11. 4 5
      services/web/app/views/project/editor/editor.pug
  12. 258 430
      services/web/npm-shrinkwrap.json
  13. 1 0
      services/web/package.json
  14. 13 1
      services/web/public/coffee/directives/asyncForm.coffee
  15. 3 0
      services/web/public/coffee/ide.coffee
  16. 9 5
      services/web/public/coffee/ide/directives/layout.coffee
  17. 3 6
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  18. 1 1
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  19. 0 58
      services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee
  20. 0 13
      services/web/public/coffee/ide/labels/LabelsManager.coffee
  21. 0 44
      services/web/public/coffee/ide/labels/services/labels.coffee
  22. 8 4
      services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee
  23. 2 1
      services/web/public/coffee/ide/share/services/projectInvites.coffee
  24. 1 0
      services/web/public/coffee/main.coffee
  25. 24 0
      services/web/public/coffee/services/validateCaptcha.coffee
  26. 4 0
      services/web/public/stylesheets/app/base.less
  27. 98 6
      services/web/public/stylesheets/app/editor.less
  28. 59 4
      services/web/public/stylesheets/app/editor/pdf.less
  29. 33 25
      services/web/public/stylesheets/core/_common-variables.less
  30. 20 15
      services/web/public/stylesheets/core/ol-variables.less
  31. 2 0
      services/web/test/unit/coffee/Email/EmailSenderTests.coffee

+ 21 - 0
services/web/app/coffee/Features/Captcha/CaptchaMiddleware.coffee

@@ -0,0 +1,21 @@
+request = require 'request'
+logger = require 'logger-sharelatex'
+Settings = require 'settings-sharelatex'
+
+module.exports = CaptchaMiddleware =
+	validateCaptcha: (req, res, next) ->
+		if !Settings.recaptcha?
+			return next()
+		response = req.body['g-recaptcha-response']
+		options =
+			form:
+				secret: Settings.recaptcha.secretKey
+				response: response
+			json: true
+		request.post "https://www.google.com/recaptcha/api/siteverify", options, (error, response, body) ->
+			return next(error) if error?
+			if !body?.success
+				logger.warn {statusCode: response.statusCode, body: body}, 'failed recaptcha siteverify request'
+				return res.sendStatus 400
+			else
+				return next()

+ 2 - 1
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -11,6 +11,7 @@ NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 AnalyticsManger = require("../Analytics/AnalyticsManager")
 AuthenticationController = require("../Authentication/AuthenticationController")
 rateLimiter = require("../../infrastructure/RateLimiter")
+request = require 'request'
 
 module.exports = CollaboratorsInviteController =
 
@@ -32,7 +33,7 @@ module.exports = CollaboratorsInviteController =
 				callback(null, userExists)
 		else
 			callback(null, true)
-	
+
 	_checkRateLimit: (user_id, callback = (error) ->) ->
 		LimitationsManager.allowedNumberOfCollaboratorsForUser user_id, (err, collabLimit = 1)->
 			return callback(err) if err?

+ 2 - 0
services/web/app/coffee/Features/Collaborators/CollaboratorsRouter.coffee

@@ -3,6 +3,7 @@ AuthenticationController = require('../Authentication/AuthenticationController')
 AuthorizationMiddlewear = require('../Authorization/AuthorizationMiddlewear')
 CollaboratorsInviteController = require('./CollaboratorsInviteController')
 RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
+CaptchaMiddleware = require '../Captcha/CaptchaMiddleware'
 
 module.exports =
 	apply: (webRouter, apiRouter) ->
@@ -32,6 +33,7 @@ module.exports =
 				maxRequests: 100
 				timeInterval: 60 * 10
 			}),
+			CaptchaMiddleware.validateCaptcha,
 			AuthenticationController.requireLogin(),
 			AuthorizationMiddlewear.ensureUserCanAdminProject,
 			CollaboratorsInviteController.inviteToProject

+ 6 - 5
services/web/app/coffee/Features/Email/EmailSender.coffee

@@ -4,6 +4,7 @@ Settings = require('settings-sharelatex')
 nodemailer = require("nodemailer")
 sesTransport = require('nodemailer-ses-transport')
 sgTransport = require('nodemailer-sendgrid-transport')
+mandrillTransport = require('nodemailer-mandrill-transport')
 rateLimiter = require('../../infrastructure/RateLimiter')
 _ = require("underscore")
 
@@ -17,22 +18,22 @@ client =
 	sendMail: (options, callback = (err,status) ->) ->
 		logger.log options:options, "Would send email if enabled."
 		callback()
-
 if Settings?.email?.parameters?.AWSAccessKeyID? or Settings?.email?.driver == 'ses'
 	logger.log "using aws ses for email"
 	nm_client = nodemailer.createTransport(sesTransport(Settings.email.parameters))
 else if Settings?.email?.parameters?.sendgridApiKey?
 	logger.log "using sendgrid for email"
 	nm_client = nodemailer.createTransport(sgTransport({auth:{api_key:Settings?.email?.parameters?.sendgridApiKey}}))
+else if Settings?.email?.parameters?.MandrillApiKey?
+	logger.log "using mandril for email"
+	nm_client = nodemailer.createTransport(mandrillTransport({auth:{apiKey:Settings?.email?.parameters?.MandrillApiKey}}))
 else if Settings?.email?.parameters?
-	smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS")
-
-
 	logger.log "using smtp for email"
+	smtp = _.pick(Settings?.email?.parameters, "host", "port", "secure", "auth", "ignoreTLS")
 	nm_client = nodemailer.createTransport(smtp)
 else
-	nm_client = client
 	logger.warn "Email transport and/or parameters not defined. No emails will be sent."
+	nm_client = client
 
 if nm_client?
 		client = nm_client

+ 0 - 28
services/web/app/coffee/Features/Labels/LabelsController.coffee

@@ -1,28 +0,0 @@
-EditorRealTimeController = require "../Editor/EditorRealTimeController"
-LabelsHandler = require './LabelsHandler'
-logger = require 'logger-sharelatex'
-
-
-module.exports = LabelsController =
-
-	getAllLabels: (req, res, next) ->
-		project_id = req.params.project_id
-		logger.log {project_id}, "getting all labels for project"
-		LabelsHandler.getAllLabelsForProject project_id, (err, projectLabels) ->
-			if err?
-				logger.err {project_id, err}, "[LabelsController] error getting all labels from project"
-				return next(err)
-			res.json {projectId: project_id, projectLabels: projectLabels}
-
-	broadcastLabelsForDoc: (req, res, next) ->
-		project_id = req.params.project_id
-		doc_id = req.params.doc_id
-		logger.log {project_id, doc_id}, "getting labels for doc"
-		LabelsHandler.getLabelsForDoc project_id, doc_id, (err, docLabels) ->
-			if err?
-				logger.err {project_id, doc_id, err}, "[LabelsController] error getting labels from doc"
-				return next(err)
-			EditorRealTimeController.emitToRoom project_id, 'broadcastDocLabels', {
-				docId: doc_id, labels: docLabels
-			}
-			res.sendStatus(200)

+ 0 - 43
services/web/app/coffee/Features/Labels/LabelsHandler.coffee

@@ -1,43 +0,0 @@
-ProjectEntityHandler = require "../Project/ProjectEntityHandler"
-DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
-
-
-module.exports = LabelsHandler =
-
-	labelCaptureRegex: () ->
-		/\\label\{([^\}\n\\]{0,80})\}/g
-
-	getAllLabelsForProject: (projectId, callback=(err, projectLabels)->) ->
-		DocumentUpdaterHandler.flushProjectToMongo projectId, (err) ->
-			if err?
-				return callback(err)
-			ProjectEntityHandler.getAllDocs projectId, (err, docs) ->
-				if err?
-					return callback(err)
-				projectLabels = LabelsHandler.extractLabelsFromProjectDocs docs
-				callback(null, projectLabels)
-
-	getLabelsForDoc: (projectId, docId, callback=(err, docLabels)->) ->
-		DocumentUpdaterHandler.flushDocToMongo projectId, docId, (err) ->
-			if err?
-				return callback(err)
-			ProjectEntityHandler.getDoc projectId, docId, (err, lines) ->
-				if err?
-					return callback(err)
-				docLabels = LabelsHandler.extractLabelsFromDoc lines
-				callback(null, docLabels)
-
-	extractLabelsFromDoc: (lines) ->
-		docLabels = []
-		for line in lines
-			re = LabelsHandler.labelCaptureRegex()
-			while (labelMatch = re.exec(line))
-				if labelMatch[1]
-					docLabels.push(labelMatch[1])
-		return docLabels
-
-	extractLabelsFromProjectDocs: (projectDocs) ->
-		projectLabels = {}  # docId => List[Label]
-		for _path, doc of projectDocs
-			projectLabels[doc._id] = LabelsHandler.extractLabelsFromDoc(doc.lines)
-		return projectLabels

+ 11 - 0
services/web/app/coffee/infrastructure/ExpressLocals.coffee

@@ -292,3 +292,14 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 		res.locals.moduleIncludes = Modules.moduleIncludes
 		res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable
 		next()
+
+	webRouter.use (req, res, next) ->
+		isOl = (Settings.brandPrefix == 'ol-')
+		res.locals.uiConfig = 
+			defaultResizerSizeOpen   : if isOl then 2 else 24
+			defaultResizerSizeClosed : if isOl then 2 else 24
+			eastResizerCursor        : if isOl then "ew-resize" else null
+			westResizerCursor        : if isOl then "ew-resize" else null
+			chatResizerSizeOpen      : if isOl then 2 else 12
+			chatResizerSizeClosed    : 0
+		next()

+ 0 - 4
services/web/app/coffee/router.coffee

@@ -44,7 +44,6 @@ SudoModeMiddlewear = require('./Features/SudoMode/SudoModeMiddlewear')
 AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter')
 AnnouncementsController = require("./Features/Announcements/AnnouncementsController")
 MetaController = require('./Features/Metadata/MetaController')
-LabelsController = require('./Features/Labels/LabelsController')
 TokenAccessController = require('./Features/TokenAccess/TokenAccessController')
 Features = require('./infrastructure/Features')
 
@@ -206,8 +205,6 @@ module.exports = class Router
 		webRouter.get '/project/:project_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.getMetadata
 		webRouter.post '/project/:project_id/doc/:doc_id/metadata', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), MetaController.broadcastMetadataForDoc
 
-		webRouter.get '/project/:project_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.getAllLabels
-		webRouter.post '/project/:project_id/doc/:doc_id/labels', AuthorizationMiddlewear.ensureUserCanReadProject, AuthenticationController.requireLogin(), LabelsController.broadcastLabelsForDoc
 
 		webRouter.get    '/tag', AuthenticationController.requireLogin(), TagsController.getAllTags
 		webRouter.post   '/tag', AuthenticationController.requireLogin(), TagsController.createTag
@@ -360,4 +357,3 @@ module.exports = class Router
 			TokenAccessController.readAndWriteToken
 
 		webRouter.get '*', ErrorController.notFound
-

+ 9 - 0
services/web/app/views/layout.pug

@@ -97,6 +97,15 @@ html(itemscope, itemtype='http://schema.org/Product')
 				}
 				
 	body
+		if(settings.recaptcha)
+			script(src="https://www.google.com/recaptcha/api.js?render=explicit")
+			div(
+				id="recaptcha"
+				class="g-recaptcha"
+				data-sitekey=settings.recaptcha.siteKey
+				data-size="invisible"
+				data-badge="inline"
+			)
 
 		- if(typeof(suppressSystemMessages) == "undefined")
 			.system-messages(

+ 3 - 2
services/web/app/views/project/editor.pug

@@ -43,8 +43,8 @@ block content
 
 		#chat-wrapper.full-size(
 			layout="chat",
-			spacing-open="12",
-			spacing-closed="0",
+			spacing-open="{{ui.chatResizerSizeOpen}}",
+			spacing-closed="{{ui.chatResizerSizeClosed}}",
 			initial-size-east="250",
 			init-closed-east="true",
 			open-east="ui.chatOpen",
@@ -158,6 +158,7 @@ block requirejs
 		window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}"
 		window.aceWorkerPath = "#{aceWorkerPath}";
 		window.pdfCMapsPath = "#{pdfCMapsPath}"
+		window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
 
 	script(
 		data-main=buildJsPath("ide.js", {fingerprint:false}),

+ 4 - 5
services/web/app/views/project/editor/editor.pug

@@ -73,21 +73,20 @@ div.full-size(
 		ng-show="!!pdf.url && settings.pdfViewer == 'pdfjs'"
 		ng-controller="PdfSynctexController"
 	)
-		a.btn.btn-default.btn-xs(
+		a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-pdf(
 			tooltip=translate('go_to_code_location_in_pdf')
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			ng-click="syncToPdf()"
 		)
-			i.fa.fa-long-arrow-right
-		br
-		a.btn.btn-default.btn-xs(
+			i.synctex-control-icon
+		a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-code(
 			tooltip-html="'"+translate('go_to_pdf_location_in_code', {}, true)+"'"
 			tooltip-placement="right"
 			tooltip-append-to-body="true"
 			ng-click="syncToCode()"
 		)
-			i.fa.fa-long-arrow-left
+			i.synctex-control-icon
 			
 div.full-size(
 	ng-if="ui.pdfLayout == 'flat'"

Разница между файлами не показана из-за своего большого размера
+ 258 - 430
services/web/npm-shrinkwrap.json


+ 1 - 0
services/web/package.json

@@ -56,6 +56,7 @@
     "multer": "^0.1.8",
     "node-html-encoder": "0.0.2",
     "nodemailer": "2.1.0",
+    "nodemailer-mandrill-transport": "^1.2.0",
     "nodemailer-sendgrid-transport": "^0.2.0",
     "nodemailer-ses-transport": "^1.3.0",
     "optimist": "0.6.1",

+ 13 - 1
services/web/public/coffee/directives/asyncForm.coffee

@@ -2,7 +2,7 @@ define [
 	"base"
 	"libs/passfield"
 ], (App) ->
-	App.directive "asyncForm", ($http) ->
+	App.directive "asyncForm", ($http, validateCaptcha) ->
 		return {
 			controller: ['$scope', ($scope) ->
 				@getEmail = () ->
@@ -17,11 +17,23 @@ define [
 
 				element.on "submit", (e) ->
 					e.preventDefault()
+					validateCaptchaIfEnabled (response) ->
+						submitRequest response
 
+				validateCaptchaIfEnabled = (callback = (response) ->) ->
+					if attrs.captcha?
+						validateCaptcha callback
+					else
+						callback()
+
+				submitRequest = (grecaptchaResponse) ->
 					formData = {}
 					for data in element.serializeArray()
 						formData[data.name] = data.value
 
+					if grecaptchaResponse?
+						formData['g-recaptcha-response'] = grecaptchaResponse
+
 					scope[attrs.name].inflight = true
 
 					# for asyncForm prevent automatic redirect to /login if

+ 3 - 0
services/web/public/coffee/ide.coffee

@@ -33,6 +33,7 @@ define [
 	"directives/expandableTextArea"
 	"directives/videoPlayState"
 	"services/queued-http"
+	"services/validateCaptcha"
 	"filters/formatDate"
 	"main/event"
 	"main/account-upgrade"
@@ -76,6 +77,8 @@ define [
 			pdfWidth: 0
 			reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
 			miniReviewPanelVisible: false
+			chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
+			chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
 		}
 		$scope.onboarding = {
 			autoCompile: if window.showAutoCompileOnboarding then 'unseen' else 'dismissed'

+ 9 - 5
services/web/public/coffee/ide/directives/layout.coffee

@@ -11,12 +11,12 @@ define [
 					if attrs.spacingOpen?
 						spacingOpen = parseInt(attrs.spacingOpen, 10)
 					else
-						spacingOpen = 24
+						spacingOpen = window.uiConfig.defaultResizerSizeOpen
 
 					if attrs.spacingClosed?
 						spacingClosed = parseInt(attrs.spacingClosed, 10)
 					else
-						spacingClosed = 24
+						spacingClosed = window.uiConfig.defaultResizerSizeClosed
 
 					options =
 						spacing_open: spacingOpen
@@ -44,6 +44,12 @@ define [
 							if !attrs.minimumRestoreSizeWest? or (state.west.size >= attrs.minimumRestoreSizeWest and !state.west.initClosed)
 								options.west = state.west
 
+					if window.uiConfig.eastResizerCursor?
+						options.east.resizerCursor = window.uiConfig.eastResizerCursor
+
+					if window.uiConfig.westResizerCursor?
+						options.west.resizerCursor = window.uiConfig.westResizerCursor
+						
 					repositionControls = () ->
 						state = element.layout().readState()
 						if state.east?
@@ -53,9 +59,7 @@ define [
 							else
 								controls.show()
 								controls.css({
-									position: "absolute"
 									right: state.east.size
-									"z-index": 3
 								})
 
 					resetOpenStates = () ->
@@ -112,7 +116,7 @@ define [
 						# Set the panel as overflowing (gives it higher z-index and sets overflow rules)
 						layoutObj.allowOverflow overflowPane
 						# Read the given z-index value and increment it, so that it's higher than synctex controls.
-						overflowPaneZVal = overflowPaneEl.css "z-index"
+						overflowPaneZVal = overflowPaneEl.zIndex()
 						overflowPaneEl.css "z-index", overflowPaneZVal + 1
 
 					resetOpenStates()

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

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

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

@@ -10,7 +10,7 @@ define [
 	aceSnippetManager = ace.require('ace/snippets').snippetManager
 
 	class AutoCompleteManager
-		constructor: (@$scope, @editor, @element, @metadataManager, @labelsManager, @graphics, @preamble, @files) ->
+		constructor: (@$scope, @editor, @element, @metadataManager, @graphics, @preamble, @files) ->
 
 			@monkeyPatchAutocomplete()
 

+ 0 - 58
services/web/public/coffee/ide/editor/directives/aceEditor/labels/LabelsManager.coffee

@@ -1,58 +0,0 @@
-define [
-	"ace/ace"
-], () ->
-	Range = ace.require("ace/range").Range
-
-	getLastCommandFragment = (lineUpToCursor) ->
-		if m = lineUpToCursor.match(/(\\[^\\]+)$/)
-			return m[1]
-		else
-			return null
-
-	class LabelsManager
-		constructor: (@$scope, @editor, @element, @Labels) ->
-			@debouncer = {}  # DocId => Timeout
-
-			onChange = (change) =>
-				if change.remote
-					return
-				if change.action not in ['remove', 'insert']
-					return
-				cursorPosition = @editor.getCursorPosition()
-				end = change.end
-				range = new Range(end.row, 0, end.row, end.column)
-				lineUpToCursor = @editor.getSession().getTextRange(range)
-				commandFragment = getLastCommandFragment(lineUpToCursor)
-				linesContainLabel = _.any(change.lines, (line) -> line.match(/\\label\{[^\}\n\\]{0,80}\}/))
-				lastCommandFragmentIsLabel = commandFragment?.slice(0,7) == '\\label{'
-				if linesContainLabel or lastCommandFragmentIsLabel
-					@scheduleLoadCurrentDocLabelsFromServer()
-
-			@editor.on "changeSession", (e) =>
-				e.oldSession.off "change", onChange
-				e.session.on "change", onChange
-
-		loadCurrentDocLabelsFromServer: () ->
-			currentDocId = @$scope.docId
-			@Labels.loadDocLabelsFromServer(currentDocId)
-
-		loadDocLabelsFromServer: (docId) ->
-			@Labels.loadDocLabelsFromServer(docId)
-
-		scheduleLoadCurrentDocLabelsFromServer: () ->
-			# De-bounce loading labels with a timeout
-			currentDocId = @$scope.docId
-			existingTimeout = @debouncer[currentDocId]
-			if existingTimeout?
-				clearTimeout(existingTimeout)
-				delete @debouncer[currentDocId]
-			@debouncer[currentDocId] = setTimeout(
-				() =>
-					@loadDocLabelsFromServer(currentDocId)
-					delete @debouncer[currentDocId]
-				, 1000
-				, this
-			)
-
-		getAllLabels: () ->
-			@Labels.getAllLabels()

+ 0 - 13
services/web/public/coffee/ide/labels/LabelsManager.coffee

@@ -1,13 +0,0 @@
-define [], () ->
-
-	class LabelsManager
-
-		constructor: (@ide, @$scope, @labels) ->
-
-			@ide.socket.on 'broadcastDocLabels', (data) =>
-				@labels.onBroadcastDocLabels(data)
-			@$scope.$on 'entity:deleted', @labels.onEntityDeleted
-			@$scope.$on 'file:upload:complete', @labels.fileUploadComplete
-
-		loadProjectLabelsFromServer: () ->
-			@labels.loadProjectLabelsFromServer()

+ 0 - 44
services/web/public/coffee/ide/labels/services/labels.coffee

@@ -1,44 +0,0 @@
-define [
-	"base"
-], (App) ->
-
-	App.factory 'labels', ($http, ide) ->
-
-		state = {documents: {}}
-
-		labels = {
-			state: state
-		}
-
-		labels.onBroadcastDocLabels = (data) ->
-			if data.docId and data.labels
-				state.documents[data.docId] = data.labels
-
-		labels.onEntityDeleted = (e, entity) ->
-			if entity.type == 'doc'
-				delete state.documents[entity.id]
-
-		labels.onFileUploadComplete = (e, upload) ->
-			if upload.entity_type == 'doc'
-				labels.loadDocLabelsFromServer(upload.entity_id)
-
-		labels.getAllLabels = () ->
-			_.flatten(labels for docId, labels of state.documents)
-
-		labels.loadProjectLabelsFromServer = () ->
-			$http
-				.get("/project/#{window.project_id}/labels")
-				.then (response) ->
-					{ data } = response
-					if data.projectLabels
-						for docId, docLabels of data.projectLabels
-							state.documents[docId] = docLabels
-
-		labels.loadDocLabelsFromServer = (docId) ->
-			$http
-				.post(
-					"/project/#{window.project_id}/doc/#{docId}/labels",
-					{_csrf: window.csrfToken}
-				)
-
-		return labels

+ 8 - 4
services/web/public/coffee/ide/share/controllers/ShareProjectModalController.coffee

@@ -1,7 +1,7 @@
 define [
 	"base"
 ], (App) ->
-	App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide) ->
+	App.controller "ShareProjectModalController", ($scope, $modalInstance, $timeout, projectMembers, projectInvites, $modal, $http, ide, validateCaptcha) ->
 		$scope.inputs = {
 			privileges: "readAndWrite"
 			contacts: []
@@ -100,7 +100,7 @@ define [
 					if email in currentInviteEmails and inviteId = _.find(($scope.project.invites || []), (invite) -> invite.email == email)?._id
 						request = projectInvites.resendInvite(inviteId)
 					else
-						request = projectInvites.sendInvite(email, $scope.inputs.privileges)
+						request = projectInvites.sendInvite(email, $scope.inputs.privileges, $scope.grecaptchaResponse)
 
 					request
 						.then (response) ->
@@ -135,7 +135,9 @@ define [
 							else
 								$scope.state.errorReason = null
 
-			$timeout addMembers, 50 # Give email list a chance to update
+			validateCaptcha (response) ->
+				$scope.grecaptchaResponse = response
+				$timeout addMembers, 50 # Give email list a chance to update
 
 		$scope.removeMember = (member) ->
 			$scope.state.error = null
@@ -210,6 +212,8 @@ define [
 		$scope.cancel = () ->
 			$modalInstance.dismiss()
 
+
+
 	App.controller "MakePublicModalController", ["$scope", "$modalInstance", "settings", ($scope, $modalInstance, settings) ->
 		$scope.inputs = {
 			privileges: "readAndWrite"
@@ -244,4 +248,4 @@ define [
 
 		$scope.cancel = () ->
 			$modalInstance.dismiss()
-	]
+	]

+ 2 - 1
services/web/public/coffee/ide/share/services/projectInvites.coffee

@@ -4,11 +4,12 @@ define [
 	App.factory "projectInvites", ["ide", "$http", (ide, $http) ->
 		return {
 
-			sendInvite: (email, privileges) ->
+			sendInvite: (email, privileges, grecaptchaResponse) ->
 				$http.post("/project/#{ide.project_id}/invite", {
 					email: email
 					privileges: privileges
 					_csrf: window.csrfToken
+					'g-recaptcha-response': grecaptchaResponse
 				})
 
 			revokeInvite: (inviteId) ->

+ 1 - 0
services/web/public/coffee/main.coffee

@@ -30,6 +30,7 @@ define [
 	"directives/maxHeight"
 	"directives/creditCards"
 	"services/queued-http"
+	"services/validateCaptcha"
 	"filters/formatDate"
 	"__MAIN_CLIENTSIDE_INCLUDES__"
 ], () ->

+ 24 - 0
services/web/public/coffee/services/validateCaptcha.coffee

@@ -0,0 +1,24 @@
+define [
+	"base"
+], (App) ->
+	App.factory "validateCaptcha", () ->
+		_recaptchaCallbacks = []
+		onRecaptchaSubmit = (token) ->
+			for cb in _recaptchaCallbacks
+				cb(token)
+			_recaptchaCallbacks = []
+
+		recaptchaId = null
+		validateCaptcha = (callback = (response) ->) =>
+			if !grecaptcha?
+				return callback()
+			reset = () ->
+				grecaptcha.reset()
+			_recaptchaCallbacks.push callback
+			_recaptchaCallbacks.push reset
+			if !recaptchaId?
+				el = $('#recaptcha')[0]
+				recaptchaId = grecaptcha.render(el, {callback: onRecaptchaSubmit})
+			grecaptcha.execute(recaptchaId)
+
+		return validateCaptcha

+ 4 - 0
services/web/public/stylesheets/app/base.less

@@ -104,3 +104,7 @@
   -ms-transform-origin: center bottom;
   transform-origin: center bottom;
 }
+
+.grecaptcha-badge {
+  display: none;
+}

+ 98 - 6
services/web/public/stylesheets/app/editor.less

@@ -14,6 +14,9 @@
 @import "./editor/review-panel.less";
 @import "./editor/feature-onboarding.less";
 
+@ui-layout-toggler-def-height: 50px;
+@ui-resizer-extra-hit-area: 8px;
+
 @keyframes blink {
 	0% { 
 		opacity: 0.2;
@@ -261,9 +264,9 @@
 	}
 }
 
-.ui-layout-resizer {
+.ui-layout-resizer when (@is-overleaf = false) {
 	width: 6px;
-	background-color: #f4f4f4;
+	background-color: @editor-resizer-bg-color;
 	border-left: 1px solid @editor-border-color;
 	border-right: 1px solid @editor-border-color;
 	.ui-layout-toggler {
@@ -276,28 +279,117 @@
 		-moz-osx-font-smoothing: grayscale;
 		font-size: 16px !important;
 		line-height: 50px;
+		background-color: @editor-toggler-bg-color;
 		&:hover {
-			background-color: #ddd;
+			background-color: @editor-toggler-hover-bg-color;
 			color: #333;
 		}
 	}
 }
-.ui-layout-resizer-west.ui-layout-resizer-open, .ui-layout-resizer-east.ui-layout-resizer-closed {
+
+.ui-layout-resizer when (@is-overleaf = true) {
+	margin-left: -(@ui-resizer-extra-hit-area) !important;
+	margin-right: -(@ui-resizer-extra-hit-area - 1px) !important;
+	padding-left: @ui-resizer-extra-hit-area !important;
+	padding-right: @ui-resizer-extra-hit-area !important;
+	z-index: 5 !important;
+	box-sizing: content-box;
+	background-image: linear-gradient(90deg, 
+		transparent,
+		transparent (@ui-resizer-extra-hit-area - 1px),
+		@editor-resizer-bg-color (@ui-resizer-extra-hit-area - 1px),
+		@editor-resizer-bg-color (@ui-resizer-extra-hit-area + 1px),
+		transparent (@ui-resizer-extra-hit-area + 1px),
+		transparent);
+
 	.ui-layout-toggler {
+		padding: 0 @ui-resizer-extra-hit-area !important;
+		background-image: linear-gradient(90deg, 
+			transparent,
+			transparent (@ui-resizer-extra-hit-area - 1px),
+			@editor-toggler-bg-color (@ui-resizer-extra-hit-area - 1px),
+			@editor-toggler-bg-color (@ui-resizer-extra-hit-area + 1px),
+			transparent (@ui-resizer-extra-hit-area + 1px),
+			transparent);
+
+		&:hover {
+			background-image: linear-gradient(90deg, 
+				transparent,
+				transparent (@ui-resizer-extra-hit-area - 2px),
+				@editor-toggler-hover-bg-color (@ui-resizer-extra-hit-area - 2px),
+				@editor-toggler-hover-bg-color (@ui-resizer-extra-hit-area + 2px),
+				transparent (@ui-resizer-extra-hit-area + 2px),
+				transparent);
+		}
+	}
+}
+
+.ui-layout-resizer-west.ui-layout-resizer-open, .ui-layout-resizer-east.ui-layout-resizer-closed {
+	.ui-layout-toggler when (@is-overleaf = false) {
 		&:before {
 			content: "\f104"
 		}
 	}
 }
 .ui-layout-resizer-east.ui-layout-resizer-open, .ui-layout-resizer-west.ui-layout-resizer-closed {
-	.ui-layout-toggler {
+	.ui-layout-toggler when (@is-overleaf = false) {
 		&:before {
 			content: "\f105"
 		}
 	}
 }
+
+.ui-layout-toggler.ui-layout-toggler-closed when (@is-overleaf = true) {
+	background-color: @editor-resizer-bg-color;
+	background-image: none;
+	line-height: @ui-layout-toggler-def-height;
+	
+	&::before {
+		content: "\22EE"; // Vertical ellipsis
+		display: block;
+		color: #FFF;
+		font-weight: 700;
+		font-size: @font-size-h2;
+		width: @ui-resizer-extra-hit-area / 2;
+	}
+	&:hover {
+		background-color: @editor-toggler-hover-bg-color;
+		background-image: none;
+	}
+	.ui-layout-resizer-west > & {
+		border-radius: 0 @border-radius-base @border-radius-base 0;
+		&::before {
+			margin-left: -2px;
+		}
+	}
+	.ui-layout-resizer-east > & {
+		border-radius: @border-radius-base 0 0 @border-radius-base;
+		&::before {
+			margin-left: (-1 - @ui-resizer-extra-hit-area);
+		}
+	}
+}
+
+.ui-layout-toggler-east when (@is-overleaf = true) {
+	&.ui-layout-toggler-open {
+		cursor: e-resize !important
+	}
+	&.ui-layout-toggler-closed {
+		cursor: w-resize !important
+	}
+}
+
+.ui-layout-toggler-west when (@is-overleaf = true) {
+	&.ui-layout-toggler-open {
+		cursor: w-resize !important
+	}
+	&.ui-layout-toggler-closed {
+		cursor: e-resize !important
+	}
+}
+
 .ui-layout-resizer-dragging {
-	background-color: #ddd;
+	background-color: @editor-resizer-bg-color-dragging;
 }
 
 .context-menu {

+ 59 - 4
services/web/public/stylesheets/app/editor/pdf.less

@@ -229,13 +229,68 @@
 }
 
 .synctex-controls {
+	position: absolute;
+	z-index: @synctex-controls-z-index;
+	padding: @synctex-controls-padding;
 	top: 68px;
-	padding: 0px 2px;
-	.btn-xs {
+}
+
+.synctex-controls when (@is-overleaf = true) {
+	margin-right: -11px;
+}
+	.synctex-control {
+		display: block;
+		margin-bottom: 3px;
+
+		> .synctex-control-icon {
+			display: inline-block;
+			font: normal normal normal 14px/1 FontAwesome;
+			font-size: inherit;
+			text-rendering: auto;
+			-webkit-font-smoothing: antialiased;
+			-moz-osx-font-smoothing: grayscale;
+		}
+	}
+
+	.synctex-control when (@is-overleaf = true) {
+		@ol-synctex-control-size: 24px;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 1em;
+		width: @ol-synctex-control-size;
+		height: @ol-synctex-control-size;
+		border-radius: @ol-synctex-control-size / 2;
+		padding: 0 0 2px;
+		background-color: fade(@btn-default-bg, 80%);
+		transition: background 0.15s ease;
+		margin-bottom: @ol-synctex-control-size / 2;
+	}
+
+	.synctex-control when (@is-overleaf = false) {
 		line-height: 1.3;
-		padding: 0 2px 0;
+		padding: 0 2px;
+	}
+	
+	.synctex-control-goto-pdf > .synctex-control-icon when (@is-overleaf = true) {
+		text-indent: 1px; // "Optical" adjustment.
+		&::before {
+			content: "\f061";		
+		}
+	}
+	.synctex-control-goto-code > .synctex-control-icon when (@is-overleaf = true) {
+		text-indent: -1px; // "Optical" adjustment.
+		&::before {
+			content: "\f060";		
+		}
+	}
+	
+	.synctex-control-goto-pdf > .synctex-control-icon::before when (@is-overleaf = false) {
+		content: "\f178";		
+	}
+	.synctex-control-goto-code > .synctex-control-icon::before when (@is-overleaf = false) {
+		content: "\f177";		
 	}
-}
 
 .editor-dark {
 	.pdf-logs {

+ 33 - 25
services/web/public/stylesheets/core/_common-variables.less

@@ -889,34 +889,42 @@
 @footer-padding                : 2em;
 
 // Editor header
-@toolbar-header-bg-color          : transparent;
-@toolbar-header-shadow            : 0 0 2px #ccc;
-@toolbar-btn-color                : @link-color;
-@toolbar-btn-hover-color          : @link-hover-color;
-@toolbar-btn-hover-bg-color       : darken(white, 10%);
-@toolbar-btn-hover-text-shadow    : 0 1px 0 rgba(0, 0, 0, 0.15);
-@toolbar-btn-active-color         : white;
-@toolbar-btn-active-bg-color      : @link-color;
-@toolbar-btn-active-shadow        : inset 0 3px 5px rgba(0, 0, 0, 0.225);
-@toolbar-alt-bg-color             : #fafafa;
-@toolbar-icon-btn-color           : @gray-light;
-@toolbar-icon-btn-hover-color     : @gray-dark;
-@toolbar-icon-btn-hover-shadow    : 0 1px 0 rgba(0, 0, 0, 0.25);
+@toolbar-header-bg-color        : transparent;
+@toolbar-header-shadow          : 0 0 2px #ccc;
+@toolbar-btn-color              : @link-color;
+@toolbar-btn-hover-color        : @link-hover-color;
+@toolbar-btn-hover-bg-color     : darken(white, 10%);
+@toolbar-btn-hover-text-shadow  : 0 1px 0 rgba(0, 0, 0, 0.15);
+@toolbar-btn-active-color       : white;
+@toolbar-btn-active-bg-color    : @link-color;
+@toolbar-btn-active-shadow      : inset 0 3px 5px rgba(0, 0, 0, 0.225);
+@toolbar-alt-bg-color           : #fafafa;
+@toolbar-icon-btn-color         : @gray-light;
+@toolbar-icon-btn-hover-color   : @gray-dark;
+@toolbar-icon-btn-hover-shadow  : 0 1px 0 rgba(0, 0, 0, 0.25);
 @toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225);
-@toolbar-border-bottom            : 1px solid @toolbar-border-color;
+@toolbar-border-bottom          : 1px solid @toolbar-border-color;
 
 // Editor file-tree
-@file-tree-bg                    : transparent;
-@file-tree-line-height           : 2.6;
-@file-tree-item-color            : @gray-darker;
-@file-tree-item-toggle-color     : @gray;
-@file-tree-item-icon-color       : @gray-light;
-@file-tree-item-input-color      : inherit;
-@file-tree-item-folder-color     : lighten(desaturate(@link-color, 10%), 5%);
-@file-tree-item-hover-bg         : @gray-lightest;
-@file-tree-item-selected-bg      : transparent;
-@file-tree-multiselect-bg        : lighten(@brand-info, 40%);
-@file-tree-multiselect-hover-bg  : lighten(@brand-info, 30%);
+@file-tree-bg                   : transparent;
+@file-tree-line-height          : 2.6;
+@file-tree-item-color           : @gray-darker;
+@file-tree-item-toggle-color    : @gray;
+@file-tree-item-icon-color      : @gray-light;
+@file-tree-item-input-color     : inherit;
+@file-tree-item-folder-color    : lighten(desaturate(@link-color, 10%), 5%);
+@file-tree-item-hover-bg        : @gray-lightest;
+@file-tree-item-selected-bg     : transparent;
+@file-tree-multiselect-bg       : lighten(@brand-info, 40%);
+@file-tree-multiselect-hover-bg : lighten(@brand-info, 30%);
+
+// Editor resizers
+@editor-resizer-bg-color          : #F4F4F4;
+@editor-resizer-bg-color-dragging : #ddd;
+@editor-toggler-bg-color          : transparent;
+@editor-toggler-hover-bg-color    : #DDD;
+@synctex-controls-z-index         : 3;
+@synctex-controls-padding         : 0 2px;
 // Tags
 @tag-border-radius  : 0.25em;
 @tag-bg-color       : @label-default-bg;

+ 20 - 15
services/web/public/stylesheets/core/ol-variables.less

@@ -174,23 +174,28 @@
 @toolbar-icon-btn-color           : #FFF;
 @toolbar-icon-btn-hover-color     : #FFF;
 @toolbar-icon-btn-hover-shadow    : none;
-@toolbar-icon-btn-hover-boxshadow : none;
 @toolbar-border-bottom            : 1px solid @toolbar-border-color;
+@toolbar-icon-btn-hover-boxshadow : none;
 // Editor file-tree
-@file-tree-bg                   : @ol-blue-gray-4;
-@file-tree-line-height          : 2.05;
-@file-tree-item-color           : #FFF;
-@file-tree-item-input-color     : @ol-blue-gray-5;
-@file-tree-item-toggle-color    : @ol-blue-gray-2;
-@file-tree-item-icon-color      : @ol-blue-gray-2;
-@file-tree-item-folder-color    : @ol-blue-gray-2;
-@file-tree-item-hover-bg        : @ol-blue-gray-5;
-@file-tree-item-selected-bg     : @ol-green;
-@file-tree-multiselect-bg       : @ol-blue;
-@file-tree-multiselect-hover-bg : @ol-dark-blue;
-@file-tree-droppable-bg-color   : tint(@ol-green, 5%);
-//== Colors
-//
+@file-tree-bg                     : @ol-blue-gray-4;
+@file-tree-line-height            : 2.05;
+@file-tree-item-color             : #FFF;
+@file-tree-item-input-color       : @ol-blue-gray-5;
+@file-tree-item-toggle-color      : @ol-blue-gray-2;
+@file-tree-item-icon-color        : @ol-blue-gray-2;
+@file-tree-item-folder-color      : @ol-blue-gray-2;
+@file-tree-item-hover-bg          : @ol-blue-gray-5;
+@file-tree-item-selected-bg       : @ol-green;
+@file-tree-multiselect-bg         : @ol-blue;
+@file-tree-multiselect-hover-bg   : @ol-dark-blue;
+@file-tree-droppable-bg-color     : tint(@ol-green, 5%);
+// Editor resizers
+@editor-resizer-bg-color          : @ol-blue-gray-6;
+@editor-resizer-bg-color-dragging : transparent;
+@editor-toggler-bg-color          : @ol-blue-gray-2;
+@editor-toggler-hover-bg-color    : @ol-green;
+@synctex-controls-z-index         : 6;
+@synctex-controls-padding         : 0;
 //## Gray and brand colors for use across Bootstrap.
 @gray-darker:           #252525;
 @gray-dark:             #505050;

+ 2 - 0
services/web/test/unit/coffee/Email/EmailSenderTests.coffee

@@ -31,6 +31,8 @@ describe "EmailSender", ->
 
 		@sender = SandboxedModule.require modulePath, requires:
 			'nodemailer': @ses
+			"nodemailer-mandrill-transport":{}
+			"nodemailer-sendgrid-transport":{}
 			"settings-sharelatex":@settings
 			'../../infrastructure/RateLimiter':@RateLimiter
 			"logger-sharelatex":

Некоторые файлы не были показаны из-за большого количества измененных файлов