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

Merge branch 'master' into dcl-i850

Conflicts repaired:
app/views/user/settings.pug

modified:   app/coffee/Features/Authentication/AuthenticationController.coffee
modified:   app/coffee/Features/Editor/EditorHttpController.coffee
modified:   app/coffee/Features/Email/EmailBuilder.coffee
modified:   app/coffee/Features/Errors/Errors.coffee
modified:   app/coffee/Features/Exports/ExportsController.coffee
modified:   app/coffee/Features/Exports/ExportsHandler.coffee
modified:   app/coffee/Features/Notifications/NotificationsBuilder.coffee
modified:   app/coffee/Features/Notifications/NotificationsHandler.coffee
modified:   app/coffee/Features/Project/ProjectController.coffee
modified:   app/coffee/Features/StaticPages/HomeController.coffee
modified:   app/coffee/Features/Subscription/planFeatures.coffee
modified:   app/coffee/Features/TokenAccess/TokenAccessController.coffee
modified:   app/coffee/Features/TokenAccess/TokenAccessHandler.coffee
modified:   app/coffee/Features/User/UserGetter.coffee
modified:   app/coffee/infrastructure/RedirectManager.coffee
modified:   app/coffee/infrastructure/Server.coffee
modified:   app/coffee/models/User.coffee
modified:   app/coffee/router.coffee
modified:   app/views/_mixins/links.pug
modified:   app/views/general/500.pug
modified:   app/views/layout/navbar.pug
modified:   app/views/project/editor/header.pug
modified:   app/views/project/editor/share.pug
modified:   app/views/project/list.pug
modified:   app/views/project/list/modals.pug
modified:   app/views/project/list/notifications.pug
modified:   app/views/project/list/side-bar.pug
modified:   app/views/project/list/v1-item.pug
modified:   app/views/subscriptions/_modal_group_inquiry.pug
modified:   app/views/subscriptions/dashboard.pug
modified:   app/views/user/settings.pug
modified:   config/settings.defaults.coffee
modified:   npm-shrinkwrap.json
modified:   package.json
new file:   public/apple-touch-icon.png
new file:   public/atlassian-domain-verification.html
modified:   public/coffee/ide/editor/directives/aceEditor.coffee
modified:   public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
modified:   public/coffee/ide/share/controllers/ShareController.coffee
modified:   public/coffee/main.coffee
deleted:    public/coffee/main/account-merge-checker.coffee
modified:   public/coffee/main/contact-us.coffee
modified:   public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
new file:   public/google4f15e48c48709a75.html
new file:   public/googleef256f97939bd9b7.html
new file:   public/img/advocates/friend.jpeg
new file:   public/img/grid.png
new file:   public/img/homepage.png
new file:   public/img/homepage@2x.png
new file:   public/img/other-brands/logo_google.svg
new file:   public/img/other-brands/logo_google_alt.svg
new file:   public/img/other-brands/logo_ieee.svg
new file:   public/img/other-brands/logo_orcid.svg
new file:   public/img/other-brands/logo_orcid_alt.svg
new file:   public/img/other-brands/logo_sharelatex.svg
new file:   public/img/other-brands/logo_twitter.svg
new file:   public/img/overleaf-partner/overleaf-greygreen-410.png
new file:   public/img/overleaf-partner/overleaf-white-410.png
new file:   public/static/brochures/Overleaf-Information-v8.pdf
new file:   public/static/brochures/Overleaf-Institutional-Solutions-v1.pdf
new file:   public/static/brochures/Overleaf-Tri-Fold-leaflet.pdf
new file:   public/static/brochures/Overleaf-request-for-service.pdf
new file:   public/static/brochures/Overleaf_Institutional.pdf
new file:   public/static/campus-challenge/campus-challenge-2016-flyer.pdf
new file:   public/static/campus-challenge/campus-challenge-2016-poster.pdf
new file:   public/static/campus-challenge/campus-challenge-2017-flyer.pdf
new file:   public/static/campus-challenge/campus-challenge-2017-poster.pdf
new file:   public/static/latex/learn/free-online-introduction-to-latex-part-1.pdf
new file:   public/static/latex/learn/free-online-introduction-to-latex-part-2.pdf
new file:   public/static/latex/learn/free-online-introduction-to-latex-part-3.pdf
new file:   public/static/partners/caltech-library.gif
new file:   public/static/partners/stanford-building.jpg
modified:   public/stylesheets/_ol_style_includes.less
modified:   public/stylesheets/app/blog-posts.less
modified:   public/stylesheets/app/cms-page.less
new file:   public/stylesheets/app/content_page.less
modified:   public/stylesheets/app/editor/share.less
modified:   public/stylesheets/app/editor/toolbar.less
modified:   public/stylesheets/app/features.less
modified:   public/stylesheets/app/homepage.less
new file:   public/stylesheets/app/login-register.less
modified:   public/stylesheets/app/plans.less
modified:   public/stylesheets/app/portals.less
modified:   public/stylesheets/app/templates-v2.less
modified:   public/stylesheets/app/wiki.less
modified:   public/stylesheets/components/alerts.less
modified:   public/stylesheets/components/buttons.less
modified:   public/stylesheets/components/icons.less
new file:   public/stylesheets/components/images.less
new file:   public/stylesheets/components/tabs.less
modified:   public/stylesheets/core/_common-variables.less
modified:   public/stylesheets/core/ol-variables.less
modified:   public/stylesheets/ol-style.less
modified:   test/acceptance/coffee/ExportsTests.coffee
new file:   test/acceptance/coffee/RedirectUrlsTests.coffee
modified:   test/acceptance/coffee/TokenAccessTests.coffee
modified:   test/acceptance/coffee/helpers/User.coffee
modified:   test/acceptance/config/settings.test.coffee
modified:   test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
modified:   test/unit/coffee/Editor/EditorHttpControllerTests.coffee
modified:   test/unit/coffee/Exports/ExportsControllerTests.coffee
modified:   test/unit/coffee/Exports/ExportsHandlerTests.coffee
new file:   test/unit/coffee/Notifications/NotificationsBuilderTests.coffee
modified:   test/unit/coffee/Project/ProjectControllerTests.coffee
modified:   test/unit/coffee/TokenAccess/TokenAccessControllerTests.coffee
modified:   test/unit/coffee/User/UserGetterTests.coffee
deleted:    test/unit/coffee/infrastructure/RedirectManagerTests.coffee
Douglas Lovell 8 лет назад
Родитель
Сommit
bfb369fab0
100 измененных файлов с 1014 добавлено и 294 удалено
  1. 10 1
      services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
  2. 11 13
      services/web/app/coffee/Features/Editor/EditorHttpController.coffee
  3. 17 0
      services/web/app/coffee/Features/Email/EmailBuilder.coffee
  4. 8 8
      services/web/app/coffee/Features/Errors/Errors.coffee
  5. 15 7
      services/web/app/coffee/Features/Exports/ExportsController.coffee
  6. 6 3
      services/web/app/coffee/Features/Exports/ExportsHandler.coffee
  7. 28 0
      services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee
  8. 5 2
      services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee
  9. 7 0
      services/web/app/coffee/Features/Project/ProjectController.coffee
  10. 8 5
      services/web/app/coffee/Features/StaticPages/HomeController.coffee
  11. 1 1
      services/web/app/coffee/Features/Subscription/planFeatures.coffee
  12. 7 1
      services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee
  13. 7 3
      services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee
  14. 2 1
      services/web/app/coffee/Features/User/UserGetter.coffee
  15. 28 15
      services/web/app/coffee/infrastructure/RedirectManager.coffee
  16. 1 1
      services/web/app/coffee/infrastructure/Server.coffee
  17. 1 0
      services/web/app/coffee/models/User.coffee
  18. 16 0
      services/web/app/coffee/router.coffee
  19. 4 8
      services/web/app/views/_mixins/links.pug
  20. 1 1
      services/web/app/views/general/500.pug
  21. 11 1
      services/web/app/views/layout/navbar.pug
  22. 0 4
      services/web/app/views/project/editor/header.pug
  23. 8 8
      services/web/app/views/project/editor/share.pug
  24. 4 1
      services/web/app/views/project/list.pug
  25. 1 1
      services/web/app/views/project/list/modals.pug
  26. 15 0
      services/web/app/views/project/list/notifications.pug
  27. 7 12
      services/web/app/views/project/list/side-bar.pug
  28. 1 1
      services/web/app/views/project/list/v1-item.pug
  29. 1 1
      services/web/app/views/subscriptions/_modal_group_inquiry.pug
  30. 2 2
      services/web/app/views/subscriptions/dashboard.pug
  31. 2 2
      services/web/app/views/user/settings.pug
  32. 4 2
      services/web/config/settings.defaults.coffee
  33. 120 5
      services/web/npm-shrinkwrap.json
  34. 5 2
      services/web/package.json
  35. BIN
      services/web/public/apple-touch-icon.png
  36. 15 0
      services/web/public/atlassian-domain-verification.html
  37. 1 0
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  38. 7 3
      services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
  39. 1 3
      services/web/public/coffee/ide/share/controllers/ShareController.coffee
  40. 0 1
      services/web/public/coffee/main.coffee
  41. 0 5
      services/web/public/coffee/main/account-merge-checker.coffee
  42. 2 2
      services/web/public/coffee/main/contact-us.coffee
  43. 11 1
      services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee
  44. 1 0
      services/web/public/google4f15e48c48709a75.html
  45. 1 0
      services/web/public/googleef256f97939bd9b7.html
  46. BIN
      services/web/public/img/advocates/friend.jpeg
  47. BIN
      services/web/public/img/grid.png
  48. BIN
      services/web/public/img/homepage.png
  49. BIN
      services/web/public/img/homepage@2x.png
  50. 1 0
      services/web/public/img/other-brands/logo_google.svg
  51. 1 0
      services/web/public/img/other-brands/logo_google_alt.svg
  52. 0 0
      services/web/public/img/other-brands/logo_ieee.svg
  53. 1 0
      services/web/public/img/other-brands/logo_orcid.svg
  54. 1 0
      services/web/public/img/other-brands/logo_orcid_alt.svg
  55. 0 0
      services/web/public/img/other-brands/logo_sharelatex.svg
  56. 1 0
      services/web/public/img/other-brands/logo_twitter.svg
  57. BIN
      services/web/public/img/overleaf-partner/overleaf-greygreen-410.png
  58. BIN
      services/web/public/img/overleaf-partner/overleaf-white-410.png
  59. BIN
      services/web/public/static/brochures/Overleaf-Information-v8.pdf
  60. 33 0
      services/web/public/static/brochures/Overleaf-Institutional-Solutions-v1.pdf
  61. BIN
      services/web/public/static/brochures/Overleaf-Tri-Fold-leaflet.pdf
  62. BIN
      services/web/public/static/brochures/Overleaf-request-for-service.pdf
  63. BIN
      services/web/public/static/brochures/Overleaf_Institutional.pdf
  64. BIN
      services/web/public/static/campus-challenge/campus-challenge-2016-flyer.pdf
  65. BIN
      services/web/public/static/campus-challenge/campus-challenge-2016-poster.pdf
  66. BIN
      services/web/public/static/campus-challenge/campus-challenge-2017-flyer.pdf
  67. BIN
      services/web/public/static/campus-challenge/campus-challenge-2017-poster.pdf
  68. BIN
      services/web/public/static/latex/learn/free-online-introduction-to-latex-part-1.pdf
  69. BIN
      services/web/public/static/latex/learn/free-online-introduction-to-latex-part-2.pdf
  70. BIN
      services/web/public/static/latex/learn/free-online-introduction-to-latex-part-3.pdf
  71. BIN
      services/web/public/static/partners/caltech-library.gif
  72. BIN
      services/web/public/static/partners/stanford-building.jpg
  73. 1 0
      services/web/public/stylesheets/_ol_style_includes.less
  74. 13 1
      services/web/public/stylesheets/app/blog-posts.less
  75. 57 0
      services/web/public/stylesheets/app/cms-page.less
  76. 9 0
      services/web/public/stylesheets/app/content_page.less
  77. 7 0
      services/web/public/stylesheets/app/editor/share.less
  78. 0 11
      services/web/public/stylesheets/app/editor/toolbar.less
  79. 0 3
      services/web/public/stylesheets/app/features.less
  80. 86 31
      services/web/public/stylesheets/app/homepage.less
  81. 195 0
      services/web/public/stylesheets/app/login-register.less
  82. 0 10
      services/web/public/stylesheets/app/plans.less
  83. 0 42
      services/web/public/stylesheets/app/portals.less
  84. 35 54
      services/web/public/stylesheets/app/templates-v2.less
  85. 1 0
      services/web/public/stylesheets/app/wiki.less
  86. 3 0
      services/web/public/stylesheets/components/alerts.less
  87. 4 0
      services/web/public/stylesheets/components/buttons.less
  88. 1 1
      services/web/public/stylesheets/components/icons.less
  89. 17 0
      services/web/public/stylesheets/components/images.less
  90. 44 0
      services/web/public/stylesheets/components/tabs.less
  91. 9 1
      services/web/public/stylesheets/core/_common-variables.less
  92. 6 5
      services/web/public/stylesheets/core/ol-variables.less
  93. 3 0
      services/web/public/stylesheets/ol-style.less
  94. 13 1
      services/web/test/acceptance/coffee/ExportsTests.coffee
  95. 34 0
      services/web/test/acceptance/coffee/RedirectUrlsTests.coffee
  96. 10 0
      services/web/test/acceptance/coffee/TokenAccessTests.coffee
  97. 1 1
      services/web/test/acceptance/coffee/helpers/User.coffee
  98. 23 0
      services/web/test/acceptance/config/settings.test.coffee
  99. 1 1
      services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee
  100. 0 5
      services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee

+ 10 - 1
services/web/app/coffee/Features/Authentication/AuthenticationController.coffee

@@ -11,6 +11,7 @@ UserHandler = require("../User/UserHandler")
 UserSessionsManager = require("../User/UserSessionsManager")
 UserSessionsManager = require("../User/UserSessionsManager")
 Analytics = require "../Analytics/AnalyticsManager"
 Analytics = require "../Analytics/AnalyticsManager"
 passport = require 'passport'
 passport = require 'passport'
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 
 
 module.exports = AuthenticationController =
 module.exports = AuthenticationController =
 
 
@@ -43,7 +44,7 @@ module.exports = AuthenticationController =
 					return callback(err)
 					return callback(err)
 				req.sessionStore.generate(req)
 				req.sessionStore.generate(req)
 				for key, value of oldSession
 				for key, value of oldSession
-					req.session[key] = value
+					req.session[key] = value unless key == '__tmp'
 				# copy to the old `session.user` location, for backward-comptability
 				# copy to the old `session.user` location, for backward-comptability
 				req.session.user = req.session.passport.user
 				req.session.user = req.session.passport.user
 				req.session.save (err) ->
 				req.session.save (err) ->
@@ -112,6 +113,7 @@ module.exports = AuthenticationController =
 		UserHandler.setupLoginData(user, ()->)
 		UserHandler.setupLoginData(user, ()->)
 		LoginRateLimiter.recordSuccessfulLogin(user.email)
 		LoginRateLimiter.recordSuccessfulLogin(user.email)
 		AuthenticationController._recordSuccessfulLogin(user._id)
 		AuthenticationController._recordSuccessfulLogin(user._id)
+		AuthenticationController.ipMatchCheck(req, user)
 		Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
 		Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
 		Analytics.identifyUser(user._id, req.sessionID)
 		Analytics.identifyUser(user._id, req.sessionID)
 		logger.log email: user.email, user_id: user._id.toString(), "successful log in"
 		logger.log email: user.email, user_id: user._id.toString(), "successful log in"
@@ -119,6 +121,13 @@ module.exports = AuthenticationController =
 		# capture the request ip for use when creating the session
 		# capture the request ip for use when creating the session
 		user._login_req_ip = req.ip
 		user._login_req_ip = req.ip
 
 
+	ipMatchCheck: (req, user) ->
+		if req.ip != user.lastLoginIp
+			NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create()
+		UserUpdater.updateUser user._id.toString(), {
+			$set: { "lastLoginIp": req.ip }
+		}
+
 	setInSessionUser: (req, props) ->
 	setInSessionUser: (req, props) ->
 		for key, value of props
 		for key, value of props
 			if req?.session?.passport?.user?
 			if req?.session?.passport?.user?

+ 11 - 13
services/web/app/coffee/Features/Editor/EditorHttpController.coffee

@@ -41,21 +41,19 @@ module.exports = EditorHttpController =
 			return callback(new Error("not found")) if !project?
 			return callback(new Error("not found")) if !project?
 			CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) ->
 			CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) ->
 				return callback(error) if error?
 				return callback(error) if error?
-				UserGetter.getUser user_id, { isAdmin: true }, (error, user) ->
+				token = TokenAccessHandler.getRequestToken(req, project_id)
+				AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) ->
 					return callback(error) if error?
 					return callback(error) if error?
-					token = TokenAccessHandler.getRequestToken(req, project_id)
-					AuthorizationManager.getPrivilegeLevelForProject user_id, project_id, token, (error, privilegeLevel) ->
+					if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
+						logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
+						return callback null, null, false
+					CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
 						return callback(error) if error?
 						return callback(error) if error?
-						if !privilegeLevel? or privilegeLevel == PrivilegeLevels.NONE
-							logger.log {project_id, user_id, privilegeLevel}, "not an acceptable privilege level, returning null"
-							return callback null, null, false
-						CollaboratorsInviteHandler.getAllInvites project_id, (error, invites) ->
-							return callback(error) if error?
-							logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
-							callback(null,
-								ProjectEditorHandler.buildProjectModelView(project, members, invites),
-								privilegeLevel
-							)
+						logger.log {project_id, user_id, memberCount: members.length, inviteCount: invites.length, privilegeLevel}, "returning project model view"
+						callback(null,
+							ProjectEditorHandler.buildProjectModelView(project, members, invites),
+							privilegeLevel
+						)
 
 
 	_nameIsAcceptableLength: (name)->
 	_nameIsAcceptableLength: (name)->
 		return name? and name.length < 150 and name.length != 0
 		return name? and name.length < 150 and name.length != 0

+ 17 - 0
services/web/app/coffee/Features/Email/EmailBuilder.coffee

@@ -40,6 +40,23 @@ The #{settings.appName} Team - #{settings.siteUrl}
 
 
 templates = {}
 templates = {}
 
 
+templates.accountMergeToOverleafAddress = CTAEmailTemplate({
+	subject: () -> "Confirm Account Merge - #{settings.appName}"
+	title: () -> "Confirm Account Merge"
+	message: () ->
+		"""
+			To merge your ShareLaTeX and Overleaf accounts, click the button below.
+			If you think you have received this message in error,
+			please contact us at https://www.overleaf.com/contact
+		"""
+	ctaText: () -> "Confirm Account Merge"
+	ctaURL: (opts) -> opts.tokenLinkUrl
+	secondaryMessage: (opts) ->
+		"If the button does not appear, open this link in your browser: #{opts.tokenLinkUrl}"
+})
+
+templates.accountMergeToSharelatexAddress = templates.accountMergeToOverleafAddress
+
 templates.registered = CTAEmailTemplate({
 templates.registered = CTAEmailTemplate({
 	subject: () -> "Activate your #{settings.appName} Account"
 	subject: () -> "Activate your #{settings.appName} Account"
 	message: (opts) -> """
 	message: (opts) -> """

+ 8 - 8
services/web/app/coffee/Features/Errors/Errors.coffee

@@ -31,56 +31,56 @@ UnsupportedFileTypeError = (message) ->
 	error.name = "UnsupportedFileTypeError"
 	error.name = "UnsupportedFileTypeError"
 	error.__proto__ = UnsupportedFileTypeError.prototype
 	error.__proto__ = UnsupportedFileTypeError.prototype
 	return error
 	return error
-UnsupportedFileTypeError.prototype.__proto___ = Error.prototype
+UnsupportedFileTypeError.prototype.__proto__ = Error.prototype
 
 
 UnsupportedBrandError = (message) ->
 UnsupportedBrandError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "UnsupportedBrandError"
 	error.name = "UnsupportedBrandError"
 	error.__proto__ = UnsupportedBrandError.prototype
 	error.__proto__ = UnsupportedBrandError.prototype
 	return error
 	return error
-UnsupportedBrandError.prototype.__proto___ = Error.prototype
+UnsupportedBrandError.prototype.__proto__ = Error.prototype
 
 
 UnsupportedExportRecordsError = (message) ->
 UnsupportedExportRecordsError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "UnsupportedExportRecordsError"
 	error.name = "UnsupportedExportRecordsError"
 	error.__proto__ = UnsupportedExportRecordsError.prototype
 	error.__proto__ = UnsupportedExportRecordsError.prototype
 	return error
 	return error
-UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype
+UnsupportedExportRecordsError.prototype.__proto__ = Error.prototype
 
 
 V1HistoryNotSyncedError = (message) ->
 V1HistoryNotSyncedError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "V1HistoryNotSyncedError"
 	error.name = "V1HistoryNotSyncedError"
 	error.__proto__ = V1HistoryNotSyncedError.prototype
 	error.__proto__ = V1HistoryNotSyncedError.prototype
 	return error
 	return error
-V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
+V1HistoryNotSyncedError.prototype.__proto__ = Error.prototype
 
 
 ProjectHistoryDisabledError = (message) ->
 ProjectHistoryDisabledError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "ProjectHistoryDisabledError"
 	error.name = "ProjectHistoryDisabledError"
 	error.__proto__ = ProjectHistoryDisabledError.prototype
 	error.__proto__ = ProjectHistoryDisabledError.prototype
 	return error
 	return error
-ProjectHistoryDisabledError.prototype.__proto___ = Error.prototype
+ProjectHistoryDisabledError.prototype.__proto__ = Error.prototype
 
 
 V1ConnectionError = (message) ->
 V1ConnectionError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "V1ConnectionError"
 	error.name = "V1ConnectionError"
 	error.__proto__ = V1ConnectionError.prototype
 	error.__proto__ = V1ConnectionError.prototype
 	return error
 	return error
-V1ConnectionError.prototype.__proto___ = Error.prototype
+V1ConnectionError.prototype.__proto__ = Error.prototype
 
 
 UnconfirmedEmailError = (message) ->
 UnconfirmedEmailError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "UnconfirmedEmailError"
 	error.name = "UnconfirmedEmailError"
 	error.__proto__ = UnconfirmedEmailError.prototype
 	error.__proto__ = UnconfirmedEmailError.prototype
 	return error
 	return error
-UnconfirmedEmailError.prototype.__proto___ = Error.prototype
+UnconfirmedEmailError.prototype.__proto__ = Error.prototype
 
 
 EmailExistsError = (message) ->
 EmailExistsError = (message) ->
 	error = new Error(message)
 	error = new Error(message)
 	error.name = "EmailExistsError"
 	error.name = "EmailExistsError"
 	error.__proto__ = EmailExistsError.prototype
 	error.__proto__ = EmailExistsError.prototype
 	return error
 	return error
-EmailExistsError.prototype.__proto___ = Error.prototype
+EmailExistsError.prototype.__proto__ = Error.prototype
 
 
 module.exports = Errors =
 module.exports = Errors =
 	NotFoundError: NotFoundError
 	NotFoundError: NotFoundError

+ 15 - 7
services/web/app/coffee/Features/Exports/ExportsController.coffee

@@ -13,13 +13,19 @@ module.exports =
 			user_id: user_id
 			user_id: user_id
 		}
 		}
 
 
-		if req.body && req.body.firstName && req.body.lastName
-			export_params.first_name = req.body.firstName.trim()
-			export_params.last_name = req.body.lastName.trim()
+		if req.body
+			export_params.first_name = req.body.firstName.trim() if req.body.firstName
+			export_params.last_name = req.body.lastName.trim() if req.body.lastName
+			# additional parameters for gallery exports
+			export_params.title = req.body.title.trim() if req.body.title
+			export_params.description = req.body.description.trim() if req.body.description
+			export_params.author = req.body.author.trim() if req.body.author
+			export_params.license = req.body.license.trim() if req.body.license
+			export_params.show_source = req.body.show_source if req.body.show_source
 
 
 		ExportsHandler.exportProject export_params, (err, export_data) ->
 		ExportsHandler.exportProject export_params, (err, export_data) ->
-			return next(err) if err?
-			logger.log 
+			return err if err?
+			logger.log
 				user_id:user_id
 				user_id:user_id
 				project_id: project_id
 				project_id: project_id
 				brand_variation_id:brand_variation_id
 				brand_variation_id:brand_variation_id
@@ -30,11 +36,13 @@ module.exports =
 	exportStatus: (req, res) ->
 	exportStatus: (req, res) ->
 		{export_id} = req.params
 		{export_id} = req.params
 		ExportsHandler.fetchExport export_id, (err, export_json) ->
 		ExportsHandler.fetchExport export_id, (err, export_json) ->
-			return next(err) if err?
+			return err if err?
 			parsed_export = JSON.parse(export_json)
 			parsed_export = JSON.parse(export_json)
 			json = {
 			json = {
 				status_summary: parsed_export.status_summary,
 				status_summary: parsed_export.status_summary,
-				status_detail: parsed_export.status_detail
+				status_detail: parsed_export.status_detail,
+				partner_submission_id: parsed_export.partner_submission_id,
+				token: parsed_export.token
 			}
 			}
 			res.send export_json: json
 			res.send export_json: json
 
 

+ 6 - 3
services/web/app/coffee/Features/Exports/ExportsHandler.coffee

@@ -20,9 +20,7 @@ module.exports = ExportsHandler = self =
 				callback null, export_data
 				callback null, export_data
 
 
 	_buildExport: (export_params, callback=(err, export_data) ->) ->
 	_buildExport: (export_params, callback=(err, export_data) ->) ->
-		project_id = export_params.project_id
-		user_id = export_params.user_id
-		brand_variation_id = export_params.brand_variation_id
+		{project_id, user_id, brand_variation_id, title, description, author, license, show_source} = export_params
 		jobs =
 		jobs =
 			project: (cb) ->
 			project: (cb) ->
 				ProjectGetter.getProject project_id, cb
 				ProjectGetter.getProject project_id, cb
@@ -60,6 +58,11 @@ module.exports = ExportsHandler = self =
 					metadata:
 					metadata:
 						compiler: project.compiler
 						compiler: project.compiler
 						imageName: project.imageName
 						imageName: project.imageName
+						title: title
+						description: description
+						author: author
+						license: license
+						show_source: show_source
 				user:
 				user:
 					id: user_id
 					id: user_id
 					firstName: user.first_name
 					firstName: user.first_name

+ 28 - 0
services/web/app/coffee/Features/Notifications/NotificationsBuilder.coffee

@@ -1,5 +1,7 @@
 logger = require("logger-sharelatex")
 logger = require("logger-sharelatex")
 NotificationsHandler = require("./NotificationsHandler")
 NotificationsHandler = require("./NotificationsHandler")
+request = require "request"
+settings = require "settings-sharelatex"
 
 
 module.exports =
 module.exports =
 
 
@@ -29,3 +31,29 @@ module.exports =
 			NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
 			NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
 		read:  (callback=()->) ->
 		read:  (callback=()->) ->
 			NotificationsHandler.markAsReadByKeyOnly @key, callback
 			NotificationsHandler.markAsReadByKeyOnly @key, callback
+
+	ipMatcherAffiliation: (userId, ip) ->
+		key: "ip-matched-affiliation-#{ip}"
+		create: (callback=()->) ->
+			return null unless settings?.apis?.v1?.url # service is not configured
+			_key = @key
+			request {
+				method: 'GET'
+				url: "#{settings.apis.v1.url}/api/v2/users/#{userId}/ip_matcher"
+				auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }
+				body: { ip: ip }
+				json: true
+				timeout: 20 * 1000
+			}, (error, response, body) ->
+				return error if error?
+				return null unless response.statusCode == 200
+
+				messageOpts =
+					university_id: body.id
+					university_name: body.name
+					content: body.enrolment_ad_html
+				logger.log user_id:userId, key:_key, "creating notification key for user"
+				NotificationsHandler.createNotification userId, _key, "notification_ip_matched_affiliation", messageOpts, null, false, callback
+
+		read: (callback = ->)->
+			NotificationsHandler.markAsReadWithKey userId, @key, callback

+ 5 - 2
services/web/app/coffee/Features/Notifications/NotificationsHandler.coffee

@@ -29,12 +29,15 @@ module.exports =
 					unreadNotifications = []
 					unreadNotifications = []
 				callback(null, unreadNotifications)
 				callback(null, unreadNotifications)
 
 
-	createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, callback)->
+	createNotification: (user_id, key, templateKey, messageOpts, expiryDateTime, forceCreate, callback)->
+		if !callback
+			callback = forceCreate
+			forceCreate = true
 		payload = {
 		payload = {
 				key:key
 				key:key
 				messageOpts:messageOpts
 				messageOpts:messageOpts
 				templateKey:templateKey
 				templateKey:templateKey
-				forceCreate: true
+				forceCreate:forceCreate
 		}
 		}
 		if expiryDateTime?
 		if expiryDateTime?
 			payload.expires = expiryDateTime
 			payload.expires = expiryDateTime

+ 7 - 0
services/web/app/coffee/Features/Project/ProjectController.coffee

@@ -26,6 +26,8 @@ TokenAccessHandler = require '../TokenAccess/TokenAccessHandler'
 CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
 CollaboratorsHandler = require '../Collaborators/CollaboratorsHandler'
 Modules = require '../../infrastructure/Modules'
 Modules = require '../../infrastructure/Modules'
 ProjectEntityHandler = require './ProjectEntityHandler'
 ProjectEntityHandler = require './ProjectEntityHandler'
+UserGetter = require("../User/UserGetter")
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 crypto = require 'crypto'
 crypto = require 'crypto'
 { V1ConnectionError } = require '../Errors/Errors'
 { V1ConnectionError } = require '../Errors/Errors'
 Features = require('../../infrastructure/Features')
 Features = require('../../infrastructure/Features')
@@ -209,6 +211,11 @@ module.exports = ProjectController =
 				user = results.user
 				user = results.user
 				warnings = ProjectController._buildWarningsList results.v1Projects
 				warnings = ProjectController._buildWarningsList results.v1Projects
 
 
+				# in v2 add notifications for matching university IPs
+				if Settings.overleaf?
+					UserGetter.getUser user_id, { 'lastLoginIp': 1 }, (error, user) ->
+						if req.ip != user.lastLoginIp
+							NotificationsBuilder.ipMatcherAffiliation(user._id, req.ip).create()
 
 
 				ProjectController._injectProjectOwners projects, (error, projects) ->
 				ProjectController._injectProjectOwners projects, (error, projects) ->
 					return next(error) if error?
 					return next(error) if error?

+ 8 - 5
services/web/app/coffee/Features/StaticPages/HomeController.coffee

@@ -9,7 +9,8 @@ fs = require "fs"
 ErrorController = require "../Errors/ErrorController"
 ErrorController = require "../Errors/ErrorController"
 AuthenticationController = require('../Authentication/AuthenticationController')
 AuthenticationController = require('../Authentication/AuthenticationController')
 
 
-homepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home.pug")
+slHomepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home/sl.pug")
+v2HomepageExists = fs.existsSync Path.resolve(__dirname + "/../../../views/external/home/v2.pug")
 
 
 module.exports = HomeController =
 module.exports = HomeController =
 	index : (req,res)->
 	index : (req,res)->
@@ -21,11 +22,13 @@ module.exports = HomeController =
 		else
 		else
 			HomeController.home(req, res)
 			HomeController.home(req, res)
 
 
-	home: (req, res)->
-		if Features.hasFeature('homepage') and homepageExists
-			res.render 'external/home'
+	home: (req, res, next)->
+		if Features.hasFeature('homepage') and !Settings.overleaf and slHomepageExists
+			res.render 'external/home/sl'
+		else if Features.hasFeature('homepage') and Settings.overleaf and v2HomepageExists
+			res.render 'external/home/v2'
 		else
 		else
-			res.redirect "/login"
+			res.redirect '/login'
 
 
 	externalPage: (page, title) ->
 	externalPage: (page, title) ->
 		return (req, res, next = (error) ->) ->
 		return (req, res, next = (error) ->) ->

+ 1 - 1
services/web/app/coffee/Features/Subscription/planFeatures.coffee

@@ -33,7 +33,7 @@ module.exports =
 			student: true
 			student: true
 		}
 		}
 		{
 		{
-			feature: 'hundreds_templates'
+			feature: 'thousands_templates'
 			value: 'bool'
 			value: 'bool'
 			info: 'hundreds_templates_info'
 			info: 'hundreds_templates_info'
 			plans: {
 			plans: {

+ 7 - 1
services/web/app/coffee/Features/TokenAccess/TokenAccessController.coffee

@@ -3,6 +3,7 @@ AuthenticationController = require '../Authentication/AuthenticationController'
 TokenAccessHandler = require './TokenAccessHandler'
 TokenAccessHandler = require './TokenAccessHandler'
 Errors = require '../Errors/Errors'
 Errors = require '../Errors/Errors'
 logger = require 'logger-sharelatex'
 logger = require 'logger-sharelatex'
+settings = require 'settings-sharelatex'
 
 
 module.exports = TokenAccessController =
 module.exports = TokenAccessController =
 
 
@@ -11,11 +12,16 @@ module.exports = TokenAccessController =
 		return ProjectController.loadEditor(req, res, next)
 		return ProjectController.loadEditor(req, res, next)
 
 
 	_tryHigherAccess: (token, userId, req, res, next) ->
 	_tryHigherAccess: (token, userId, req, res, next) ->
-		TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project) ->
+		TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project, projectExists) ->
 			if err?
 			if err?
 				logger.err {err, token, userId},
 				logger.err {err, token, userId},
 					"[TokenAccess] error finding project with higher access"
 					"[TokenAccess] error finding project with higher access"
 				return next(err)
 				return next(err)
+			if !projectExists and settings.overleaf
+				logger.log {token, userId},
+					"[TokenAccess] no project found for this token"
+				# Project does not exist, but may be unimported - try it on v1
+				return res.redirect(settings.overleaf.host + req.url)
 			if !project?
 			if !project?
 				logger.log {token, userId},
 				logger.log {token, userId},
 					"[TokenAccess] no project with higher access found for this user and token"
 					"[TokenAccess] no project with higher access found for this user and token"

+ 7 - 3
services/web/app/coffee/Features/TokenAccess/TokenAccessHandler.coffee

@@ -22,7 +22,7 @@ module.exports = TokenAccessHandler =
 			'publicAccesLevel': PublicAccessLevels.TOKEN_BASED
 			'publicAccesLevel': PublicAccessLevels.TOKEN_BASED
 		}, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback
 		}, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback
 
 
-	findProjectWithHigherAccess: (token, userId, callback=(err, project)->) ->
+	findProjectWithHigherAccess: (token, userId, callback=(err, project, projectExists)->) ->
 		Project.findOne {
 		Project.findOne {
 			$or: [
 			$or: [
 				{'tokens.readAndWrite': token},
 				{'tokens.readAndWrite': token},
@@ -32,12 +32,16 @@ module.exports = TokenAccessHandler =
 			if err?
 			if err?
 				return callback(err)
 				return callback(err)
 			if !project?
 			if !project?
-				return callback(null, null)
+				return callback(null, null, false) # Project doesn't exist, so we handle differently
 			projectId = project._id
 			projectId = project._id
 			CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, (err, isMember) ->
 			CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, (err, isMember) ->
 				if err?
 				if err?
 					return callback(err)
 					return callback(err)
-				callback(null, if isMember == true then project else null)
+				callback(
+					null,
+					if isMember == true then project else null,
+					true # Project does exist, but user doesn't have access
+				)
 
 
 	addReadOnlyUserToProject: (userId, projectId, callback=(err)->) ->
 	addReadOnlyUserToProject: (userId, projectId, callback=(err)->) ->
 		userId = ObjectId(userId.toString())
 		userId = ObjectId(userId.toString())

+ 2 - 1
services/web/app/coffee/Features/User/UserGetter.coffee

@@ -8,6 +8,7 @@ Errors = require("../Errors/Errors")
 
 
 module.exports = UserGetter =
 module.exports = UserGetter =
 	getUser: (query, projection, callback = (error, user) ->) ->
 	getUser: (query, projection, callback = (error, user) ->) ->
+		return callback(new Error("no query provided")) unless query?
 		if query?.email?
 		if query?.email?
 			return callback(new Error("Don't use getUser to find user by email"), null)
 			return callback(new Error("Don't use getUser to find user by email"), null)
 		if arguments.length == 2
 		if arguments.length == 2
@@ -34,7 +35,7 @@ module.exports = UserGetter =
 
 
 			getUserAffiliations userId, (error, affiliationsData) ->
 			getUserAffiliations userId, (error, affiliationsData) ->
 				return callback error if error?
 				return callback error if error?
-				callback null, decorateFullEmails(user.email, user.emails, affiliationsData)
+				callback null, decorateFullEmails(user.email, user.emails or [], affiliationsData)
 
 
 	getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
 	getUserByMainEmail: (email, projection, callback = (error, user) ->) ->
 		email = email.trim()
 		email = email.trim()

+ 28 - 15
services/web/app/coffee/infrastructure/RedirectManager.coffee

@@ -1,20 +1,33 @@
 settings = require("settings-sharelatex")
 settings = require("settings-sharelatex")
 logger = require("logger-sharelatex")
 logger = require("logger-sharelatex")
 
 
-module.exports = (req, res, next)->
-	
-	requestedUrl = req.url
+module.exports = RedirectManager =
+	apply: (webRouter) ->
+		for redirectUrl, target of settings.redirects
+			for method in (target.methods or ['get'])
+				webRouter[method] redirectUrl, RedirectManager.createRedirect(target)
 
 
-	redirectUrl = settings.redirects[requestedUrl]
-
-	#remove starting slash
-	if !redirectUrl? and requestedUrl[requestedUrl.length-1] == "/"
-		requestedUrl = requestedUrl.substring(0, requestedUrl.length - 1)
-		redirectUrl = settings.redirects[requestedUrl]
-
-	if redirectUrl?
-		logger.log redirectUrl:redirectUrl, reqUrl:req.url, "redirecting to new path"
-		res.redirect 301, "#{redirectUrl}"
-	else
-		next()
+	createRedirect: (target) ->
+		(req, res, next) ->
+			code = 302
+			if typeof target is 'string'
+				url = target
+			else
+				if req.method == "POST"
+					code = 307
+				if typeof target.url == "function"
+					url = target.url(req.params)
+					if !url
+						return next()
+				else
+					url = target.url
+				if target.baseUrl?
+					url = "#{target.baseUrl}#{url}"
+			res.redirect code, url + getQueryString(req)
 
 
+# Naively get the query params string. Stringifying the req.query object may
+# have differences between Express and Rails, so safer to just pass the raw
+# string
+getQueryString = (req) ->
+	qs = req.url.match(/\?.*$/)
+	if qs? then qs[0] else ""

+ 1 - 1
services/web/app/coffee/infrastructure/Server.coffee

@@ -73,7 +73,7 @@ app.use multer(dest: Settings.path.uploadFolder)
 app.use methodOverride()
 app.use methodOverride()
 
 
 app.use metrics.http.monitor(logger)
 app.use metrics.http.monitor(logger)
-app.use RedirectManager
+RedirectManager.apply(webRouter)
 ProxyManager.apply(publicApiRouter)
 ProxyManager.apply(publicApiRouter)
 
 
 
 

+ 1 - 0
services/web/app/coffee/models/User.coffee

@@ -22,6 +22,7 @@ UserSchema = new Schema
 	confirmed         : {type : Boolean, default : false}
 	confirmed         : {type : Boolean, default : false}
 	signUpDate        : {type : Date, default: () -> new Date() }
 	signUpDate        : {type : Date, default: () -> new Date() }
 	lastLoggedIn      : {type : Date}
 	lastLoggedIn      : {type : Date}
+	lastLoginIp       : {type : String, default : ''}
 	loginCount        : {type : Number, default: 0}
 	loginCount        : {type : Number, default: 0}
 	holdingAccount    : {type : Boolean, default: false}
 	holdingAccount    : {type : Boolean, default: false}
 	ace               : {
 	ace               : {

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

@@ -68,6 +68,7 @@ module.exports = class Router
 		webRouter.get  '/logout', UserController.logout
 		webRouter.get  '/logout', UserController.logout
 		webRouter.get  '/restricted', AuthorizationMiddlewear.restricted
 		webRouter.get  '/restricted', AuthorizationMiddlewear.restricted
 
 
+
 		if Features.hasFeature('registration')
 		if Features.hasFeature('registration')
 			webRouter.get '/register', UserPagesController.registerPage
 			webRouter.get '/register', UserPagesController.registerPage
 			AuthenticationController.addEndpointToLoginWhitelist '/register'
 			AuthenticationController.addEndpointToLoginWhitelist '/register'
@@ -329,6 +330,21 @@ module.exports = class Router
 			AuthenticationController.httpAuth,
 			AuthenticationController.httpAuth,
 			CompileController.getFileFromClsiWithoutUser
 			CompileController.getFileFromClsiWithoutUser
 
 
+		webRouter.get '/teams', (req, res, next) ->
+			# Match v1 behaviour - if the user is signed in, show their teams list
+			# Otherwise show some information about teams
+			if AuthenticationController.isUserLoggedIn(req)
+				res.redirect('/user/subscription')
+			else
+				res.redirect("#{settings.v1Api.host}/teams")
+
+		webRouter.get '/chrome', (req, res, next) ->
+			# Match v1 behaviour - this is used for a Chrome web app
+			if AuthenticationController.isUserLoggedIn(req)
+				res.redirect('/project')
+			else
+				res.redirect('/register')
+
 		#Admin Stuff
 		#Admin Stuff
 		webRouter.get  '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index
 		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
 		webRouter.get  '/admin/user', AuthorizationMiddlewear.ensureUserIsSiteAdmin, (req, res)-> res.redirect("/admin/register") #this gets removed by admin-panel addon

+ 4 - 8
services/web/app/views/_mixins/links.pug

@@ -18,8 +18,7 @@ mixin linkAdvisors(linkText, linkClass, track)
 		| #{linkText ? linkText : 'advisor programme'}
 		| #{linkText ? linkText : 'advisor programme'}
 
 
 mixin linkBenefits(linkText, linkClass)
 mixin linkBenefits(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/benefits" class=linkClass ? linkClass : '')
+	a(href="/for/authors" class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'benefits'}
 		| #{linkText ? linkText : 'benefits'}
 		
 		
 mixin linkBlog(linkText, linkClass, slug)
 mixin linkBlog(linkText, linkClass, slug)
@@ -36,8 +35,7 @@ mixin linkDash(linkText, linkClass)
 		| #{linkText ? linkText : 'project dashboard'}
 		| #{linkText ? linkText : 'project dashboard'}
 
 
 mixin linkEducation(linkText, linkClass)
 mixin linkEducation(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/plans" class=linkClass ? linkClass : '')
+	a(href="/for/edu" class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'teaching toolkit'}
 		| #{linkText ? linkText : 'teaching toolkit'}
 
 
 mixin linkEmail(linkText, linkClass, email)
 mixin linkEmail(linkText, linkClass, email)
@@ -66,8 +64,7 @@ mixin linkInvite(linkText, linkClass, track)
 		| #{linkText ? linkText : 'invite your friends'}
 		| #{linkText ? linkText : 'invite your friends'}
 
 
 mixin linkPlansAndPricing(linkText, linkClass)
 mixin linkPlansAndPricing(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/plans" class=linkClass ? linkClass : '')
+	a(href="/user/subscription/plans" class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'plans and pricing'}
 		| #{linkText ? linkText : 'plans and pricing'}
 
 
 mixin linkPrintNewTab(linkText, linkClass, icon, track)
 mixin linkPrintNewTab(linkText, linkClass, icon, track)
@@ -121,6 +118,5 @@ mixin linkTweet(linkText, linkClass, tweetText, track)
 	) #{linkText ? linkText : 'tweet'}
 	) #{linkText ? linkText : 'tweet'}
 
 
 mixin linkUniversities(linkText, linkClass)
 mixin linkUniversities(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/universities" class=linkClass ? linkClass : '')
+	a(href="/for/universities" class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'universities'}
 		| #{linkText ? linkText : 'universities'}

+ 1 - 1
services/web/app/views/general/500.pug

@@ -2,7 +2,7 @@ doctype html
 html.full-height(itemscope, itemtype='http://schema.org/Product')
 html.full-height(itemscope, itemtype='http://schema.org/Product')
 	head
 	head
 		title Something went wrong
 		title Something went wrong
-		link(rel="icon", href="/favicon.ico")
+		link(rel="icon", href="/" + settings.brandPrefix + "favicon.ico")
 		if buildCssPath
 		if buildCssPath
 			link(rel="stylesheet", href=buildCssPath("/" + settings.brandPrefix + "style.css"))
 			link(rel="stylesheet", href=buildCssPath("/" + settings.brandPrefix + "style.css"))
 		link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
 		link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")

+ 11 - 1
services/web/app/views/layout/navbar.pug

@@ -27,8 +27,18 @@ nav.navbar.navbar-default.navbar-main
 
 
 				// loop over header_extras
 				// loop over header_extras
 				each item in nav.header_extras
 				each item in nav.header_extras
+					-
+						if ((item.only_when_logged_in && getSessionUser())
+							|| (item.only_when_logged_out && (!getSessionUser()))
+							|| (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
+							|| (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
+						){ 
+							var showNavItem = true
+						} else {
+							var showNavItem = false
+						}
 
 
-					if ((item.only_when_logged_in && getSessionUser()) || (item.only_when_logged_out && (!getSessionUser())) || (!item.only_when_logged_out && !item.only_when_logged_in))
+					if showNavItem
 						if item.dropdown
 						if item.dropdown
 							li.dropdown(class=item.class, dropdown)
 							li.dropdown(class=item.class, dropdown)
 								a.dropdown-toggle(href, dropdown-toggle)
 								a.dropdown-toggle(href, dropdown-toggle)

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

@@ -103,12 +103,8 @@ header.toolbar.toolbar-header.toolbar-with-labels(
 
 
 		a.btn.btn-full-height(
 		a.btn.btn-full-height(
 			href
 			href
-			ng-class="{ 'btn-full-height-disabled' : !permissions.admin }"
 			ng-click="openShareProjectModal(permissions.admin);"
 			ng-click="openShareProjectModal(permissions.admin);"
 			ng-controller="ShareController"
 			ng-controller="ShareController"
-			tooltip-enable="!permissions.admin"
-			tooltip="Only the project owner can use the Share menu at the moment, but we're working on making it accessible to collaborators, too."
-			tooltip-placement="bottom"
 		)
 		)
 			i.fa.fa-fw.fa-group
 			i.fa.fa-fw.fa-group
 			p.toolbar-label #{translate("share")}
 			p.toolbar-label #{translate("share")}

+ 8 - 8
services/web/app/views/project/editor/share.pug

@@ -8,9 +8,8 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 		h3 #{translate("share_project")}
 		h3 #{translate("share_project")}
 	.modal-body.modal-body-share
 	.modal-body.modal-body-share
 		.container-fluid
 		.container-fluid
-
 			//- Private (with token-access available)
 			//- Private (with token-access available)
-			.row.public-access-level(ng-show="project.publicAccesLevel == 'private'")
+			.row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'private'")
 				.col-xs-12.text-center
 				.col-xs-12.text-center
 					| #{translate('link_sharing_is_off')}
 					| #{translate('link_sharing_is_off')}
 					| &nbsp;&nbsp;
 					| &nbsp;&nbsp;
@@ -28,7 +27,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						)
 						)
 
 
 			//- Token-based access
 			//- Token-based access
-			.row.public-access-level(ng-show="project.publicAccesLevel == 'tokenBased'")
+			.row.public-access-level(ng-show="isAdmin && project.publicAccesLevel == 'tokenBased'")
 				.col-xs-12.text-center
 				.col-xs-12.text-center
 					strong
 					strong
 						| #{translate('link_sharing_is_on')}.
 						| #{translate('link_sharing_is_on')}.
@@ -57,7 +56,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}...
 						pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}...
 
 
 			//- legacy public-access
 			//- legacy public-access
-			.row.public-access-level(ng-show="project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly'")
+			.row.public-access-level(ng-show="isAdmin && (project.publicAccesLevel == 'readAndWrite' || project.publicAccesLevel == 'readOnly')")
 				.col-xs-12.text-center
 				.col-xs-12.text-center
 					strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")}
 					strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")}
 					strong(ng-if="project.publicAccesLevel == 'readOnly'") #{translate("this_project_is_public_read_only")}
 					strong(ng-if="project.publicAccesLevel == 'readOnly'") #{translate("this_project_is_public_read_only")}
@@ -77,7 +76,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 				.col-xs-3.text-left
 				.col-xs-3.text-left
 					span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
 					span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
 					span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
 					span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
-				.col-xs-1
+				.col-xs-1(ng-show="isAdmin")
 					a(
 					a(
 						href
 						href
 						tooltip=translate('remove_collaborator')
 						tooltip=translate('remove_collaborator')
@@ -102,7 +101,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						ng-click="revokeInvite(invite)"
 						ng-click="revokeInvite(invite)"
 					)
 					)
 						i.fa.fa-times
 						i.fa.fa-times
-			.row.invite-controls
+			.row.invite-controls(ng-show="isAdmin")
 				form(ng-show="canAddCollaborators")
 				form(ng-show="canAddCollaborators")
 					.small #{translate("share_with_your_collabs")}
 					.small #{translate("share_with_your_collabs")}
 					.form-group
 					.form-group
@@ -177,8 +176,9 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						) #{translate("start_free_trial")}
 						) #{translate("start_free_trial")}
 
 
 					p.small(ng-show="startedFreeTrial")
 					p.small(ng-show="startedFreeTrial")
-						| #{translate("refresh_page_after_starting_free_trial")}.
-
+						| #{translate("refresh_page_after_starting_free_trial")}
+			.row.public-access-level.public-access-level--notice(ng-show="!isAdmin")
+				.col-xs-12.text-center #{translate("to_add_more_collaborators")}
 	.modal-footer.modal-footer-share
 	.modal-footer.modal-footer-share
 		.modal-footer-left
 		.modal-footer-left
 			i.fa.fa-refresh.fa-spin(ng-show="state.inflight")
 			i.fa.fa-refresh.fa-spin(ng-show="state.inflight")

+ 4 - 1
services/web/app/views/project/list.pug

@@ -1,5 +1,8 @@
 extends ../layout
 extends ../layout
 
 
+block vars
+	- var suppressNavContentLinks = true
+
 block content
 block content
 	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
 	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
 	//- and doesn't prematurely end the script tag.
 	//- and doesn't prematurely end the script tag.
@@ -98,7 +101,7 @@ block content
 										| To tag or rename your v1 projects, please go back to Overleaf v1.
 										| To tag or rename your v1 projects, please go back to Overleaf v1.
 									div(ng-show="visible")
 									div(ng-show="visible")
 										a.project-list-sidebar-v1-link(
 										a.project-list-sidebar-v1-link(
-											href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
+											href='/sign_in_to_v1?return_to=/dash%3Fprefer-v1-dash%3D1'
 										) Go back to v1
 										) Go back to v1
 								if userIsFromSL(user)
 								if userIsFromSL(user)
 									div(ng-show="visible")
 									div(ng-show="visible")

+ 1 - 1
services/web/app/views/project/list/modals.pug

@@ -356,7 +356,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate")
 			form-messages(for="v1ImportForm")
 			form-messages(for="v1ImportForm")
 			if settings.overleaf && settings.overleaf.host
 			if settings.overleaf && settings.overleaf.host
 				a.btn.btn-primary.v1-import-btn(
 				a.btn.btn-primary.v1-import-btn(
-					ng-href=settings.overleaf.host + "/{{project.id}}"
+					ng-href='/sign_in_to_v1?return_to=/{{project.id}}'
 					ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.response.success}"
 					ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.response.success}"
 				) No thanks, open in v1
 				) No thanks, open in v1
 			input.btn.btn-primary.v1-import-btn(
 			input.btn.btn-primary.v1-import-btn(

+ 15 - 0
services/web/app/views/project/list/notifications.pug

@@ -60,6 +60,21 @@ span(ng-controller="NotificationsController").userNotifications
 									button(ng-click="dismiss(notification)").close.pull-right
 									button(ng-click="dismiss(notification)").close.pull-right
 										span(aria-hidden="true") &times;
 										span(aria-hidden="true") &times;
 										span.sr-only #{translate("close")}
 										span.sr-only #{translate("close")}
+						.alert.alert-info(ng-switch-when="notification_ip_matched_affiliation")
+							div.notification_inner
+								.notification_body
+									| It looks like you're at 
+									strong {{ notification.messageOpts.university_name }}! <br/>
+									|	Did you know that {{notification.messageOpts.university_name}} is providing
+									strong  free Overleaf Professional accounts 
+									|	 to everyone at {{notification.messageOpts.university_name}}? <br/>
+									|	Add an institutional email address to claim your account.
+									a.pull-right.btn.btn-sm.btn-info(href="/user/settings")
+										| Add Affiliation
+								span().notification_close
+									button(ng-click="dismiss(notification)").close.pull-right
+										span(aria-hidden="true") &times;
+										span.sr-only #{translate("close")}
 						.alert.alert-info(ng-switch-default)
 						.alert.alert-info(ng-switch-default)
 							div.notification_inner
 							div.notification_inner
 								span(ng-bind-html="notification.html").notification_body
 								span(ng-bind-html="notification.html").notification_body

+ 7 - 12
services/web/app/views/project/list/side-bar.pug

@@ -115,19 +115,14 @@
 	span(ng-controller="LeftHandMenuPromoController",  ng-cloak)
 	span(ng-controller="LeftHandMenuPromoController",  ng-cloak)
 
 
 		.row-spaced#userProfileInformation(ng-if="hasProjects")
 		.row-spaced#userProfileInformation(ng-if="hasProjects")
-			div(ng-controller="UserProfileController")
-				hr(ng-show="percentComplete < 100")
-				.text-centered.user-profile(ng-show="percentComplete < 100")
-					.progress
-						.progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}")
-
-					p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})}
+			div(ng-show="userEmails.length > 0 && userAffiliations.length == 0", ng-cloak)
+				hr
+				.text-centered.user-profile
+					p Are you affiliated with an institution?
 					
 					
-					button#completeUserProfileInformation.btn.btn-info(
-						ng-hide="formVisable",
-						ng-click="openUserProfileModal()"
-					) #{translate("complete")}
-
+					a.btn.btn-info(
+						href="/user/settings"
+					) Add Affiliation
 
 
 		.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered
 		.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered
 			hr
 			hr

+ 1 - 1
services/web/app/views/project/list/v1-item.pug

@@ -13,7 +13,7 @@
 				ng-show="project.accessLevel == 'owner'"
 				ng-show="project.accessLevel == 'owner'"
 			) {{project.name}}
 			) {{project.name}}
 			a.projectName(
 			a.projectName(
-				href=settings.overleaf.host + "/{{project.id}}"
+				href='/sign_in_to_v1?return_to=/{{project.id}}'
 				target="_blank"
 				target="_blank"
 				ng-hide="project.accessLevel == 'owner'"
 				ng-hide="project.accessLevel == 'owner'"
 			) {{project.name}}
 			) {{project.name}}

+ 1 - 1
services/web/app/views/subscriptions/_modal_group_inquiry.pug

@@ -2,7 +2,7 @@ script(type="text/ng-template", id="groupPlanModalTemplate")
 	.modal-header
 	.modal-header
 		h3 #{translate("group_plan_enquiry")}
 		h3 #{translate("group_plan_enquiry")}
 	.modal-body
 	.modal-body
-		form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()")
+		form.text-left.form(ng-controller="GroupPlanContactController", ng-submit="contactUs()")
 			span(ng-show="sent == false && error == false")
 			span(ng-show="sent == false && error == false")
 				.form-group
 				.form-group
 					label#title9(for='Field9')
 					label#title9(for='Field9')

+ 2 - 2
services/web/app/views/subscriptions/dashboard.pug

@@ -122,7 +122,7 @@ block content
 							p
 							p
 								| You are subscribed to Overleaf through Overleaf v1
 								| You are subscribed to Overleaf through Overleaf v1
 							p
 							p
-								a.btn.btn-primary(href=settings.overleaf.host+"/users/edit#status") Manage v1 Subscription
+								a.btn.btn-primary(href='/sign_in_to_v1?return_to=/users/edit%23status') Manage v1 Subscription
 							hr
 							hr
 
 
 						if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0
 						if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0
@@ -130,7 +130,7 @@ block content
 								p
 								p
 									| You are a member of the Overleaf v1 team: <strong>#{team.name}</strong>
 									| You are a member of the Overleaf v1 team: <strong>#{team.name}</strong>
 							p
 							p
-								a.btn.btn-primary(href=settings.overleaf.host+"/teams") Manage v1 Team Membership
+								a.btn.btn-primary(href="/sign_in_to_v1?return_to=/teams") Manage v1 Team Membership
 							hr
 							hr
 
 
 					.card(ng-if="view == 'cancelation'")
 					.card(ng-if="view == 'cancelation'")

+ 2 - 2
services/web/app/views/user/settings.pug

@@ -197,7 +197,7 @@ block content
 			if settings.createV1AccountOnLogin && settings.overleaf
 			if settings.createV1AccountOnLogin && settings.overleaf
 				p
 				p
 					strong
 					strong
-						| This will also delete your user account on #[a(href=settings.overleaf.host target="_blank") Overleaf v1].
+						| This will also delete your user account on #[a(href='/sign_in_to_v1?return_to=/dash%3Fprefer-v1-dash%3D1' target="_blank") Overleaf v1].
 						| If you want to remove your projects from Overleaf v1, you must do this before you
 						| If you want to remove your projects from Overleaf v1, you must do this before you
 						| delete your account by going to your My Projects page in Overleaf v1, moving your
 						| delete your account by going to your My Projects page in Overleaf v1, moving your
 						| projects to the Trash, and then from there either ‘leaving’ or ‘purging’ them, as appropriate.
 						| projects to the Trash, and then from there either ‘leaving’ or ‘purging’ them, as appropriate.
@@ -248,7 +248,7 @@ block content
 					div.alert.alert-info
 					div.alert.alert-info
 						| If you can't remember your password, or if you are using Single-Sign-On with another provider
 						| If you can't remember your password, or if you are using Single-Sign-On with another provider
 						| to sign in (such as Twitter or Google), please
 						| to sign in (such as Twitter or Google), please
-						| #[a(href=settings.overleaf.host+'/users/password/new', target='_blank') reset your password],
+						| #[a(href='/sign_in_to_v1?return_to=/users/password/new', target='_blank') reset your password],
 						| and try again.
 						| and try again.
 		.modal-footer
 		.modal-footer
 			button.btn.btn-default(
 			button.btn.btn-default(

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

@@ -491,7 +491,7 @@ module.exports = settings =
 	modules:
 	modules:
 		sanitize:
 		sanitize:
 			options: 
 			options: 
-				allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe', 'img', 'figure', 'figcaption' ]
+				allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe', 'img', 'figure', 'figcaption', 'source', 'video' ]
 				allowedAttributes:
 				allowedAttributes:
 					'a': [ 'href', 'name', 'target', 'class' ]
 					'a': [ 'href', 'name', 'target', 'class' ]
 					'div': [ 'class', 'id', 'style' ]
 					'div': [ 'class', 'id', 'style' ]
@@ -504,4 +504,6 @@ module.exports = settings =
 					'figure': [ 'class', 'id', 'style']
 					'figure': [ 'class', 'id', 'style']
 					'figcaption': [ 'class', 'id', 'style']
 					'figcaption': [ 'class', 'id', 'style']
 					'iframe': [ 'allowfullscreen', 'frameborder', 'height', 'src', 'width' ]
 					'iframe': [ 'allowfullscreen', 'frameborder', 'height', 'src', 'width' ]
-					'img': [ 'alt', 'class', 'src', 'style' ]
+					'img': [ 'alt', 'class', 'src', 'style' ]
+					'source': [ 'src', 'type' ]
+					'video': [ 'alt', 'class', 'controls', 'height', 'width' ]

+ 120 - 5
services/web/npm-shrinkwrap.json

@@ -1545,6 +1545,11 @@
         }
         }
       }
       }
     },
     },
+    "chownr": {
+      "version": "1.1.1",
+      "from": "chownr@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz"
+    },
     "ci-info": {
     "ci-info": {
       "version": "1.1.3",
       "version": "1.1.3",
       "from": "ci-info@>=1.0.0 <2.0.0",
       "from": "ci-info@>=1.0.0 <2.0.0",
@@ -3482,6 +3487,12 @@
       "resolved": "https://registry.npmjs.org/flatiron/-/flatiron-0.4.3.tgz",
       "resolved": "https://registry.npmjs.org/flatiron/-/flatiron-0.4.3.tgz",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
+        "minimist": {
+          "version": "0.0.10",
+          "from": "minimist@>=0.0.1 <0.1.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+          "dev": true
+        },
         "optimist": {
         "optimist": {
           "version": "0.6.0",
           "version": "0.6.0",
           "from": "optimist@0.6.0",
           "from": "optimist@0.6.0",
@@ -3603,6 +3614,11 @@
       "from": "fs-extra@>=4.0.2 <5.0.0",
       "from": "fs-extra@>=4.0.2 <5.0.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz"
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz"
     },
     },
+    "fs-minipass": {
+      "version": "1.2.5",
+      "from": "fs-minipass@>=1.2.5 <2.0.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz"
+    },
     "fs.realpath": {
     "fs.realpath": {
       "version": "1.0.0",
       "version": "1.0.0",
       "from": "fs.realpath@>=1.0.0 <2.0.0",
       "from": "fs.realpath@>=1.0.0 <2.0.0",
@@ -6696,6 +6712,38 @@
       "from": "lynx@0.1.1",
       "from": "lynx@0.1.1",
       "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz"
       "resolved": "https://registry.npmjs.org/lynx/-/lynx-0.1.1.tgz"
     },
     },
+    "mailchimp-api-v3": {
+      "version": "1.12.0",
+      "from": "mailchimp-api-v3@>=1.12.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/mailchimp-api-v3/-/mailchimp-api-v3-1.12.0.tgz",
+      "dependencies": {
+        "bluebird": {
+          "version": "3.5.2",
+          "from": "bluebird@>=3.4.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz"
+        },
+        "lodash": {
+          "version": "4.17.11",
+          "from": "lodash@>=4.17.10 <5.0.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz"
+        },
+        "safe-buffer": {
+          "version": "5.1.2",
+          "from": "safe-buffer@>=5.1.2 <6.0.0",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
+        },
+        "tar": {
+          "version": "4.4.6",
+          "from": "tar@>=4.0.2 <5.0.0",
+          "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz"
+        },
+        "yallist": {
+          "version": "3.0.2",
+          "from": "yallist@>=3.0.2 <4.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz"
+        }
+      }
+    },
     "mailcomposer": {
     "mailcomposer": {
       "version": "3.3.2",
       "version": "3.3.2",
       "from": "mailcomposer@3.3.2",
       "from": "mailcomposer@3.3.2",
@@ -6981,9 +7029,31 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
     },
     },
     "minimist": {
     "minimist": {
-      "version": "0.0.8",
-      "from": "minimist@0.0.8",
-      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+      "version": "1.2.0",
+      "from": "minimist@1.2.0",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+    },
+    "minipass": {
+      "version": "2.3.4",
+      "from": "minipass@>=2.3.3 <3.0.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz",
+      "dependencies": {
+        "safe-buffer": {
+          "version": "5.1.2",
+          "from": "safe-buffer@^5.1.2",
+          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
+        },
+        "yallist": {
+          "version": "3.0.2",
+          "from": "yallist@^3.0.0",
+          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz"
+        }
+      }
+    },
+    "minizlib": {
+      "version": "1.1.0",
+      "from": "minizlib@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz"
     },
     },
     "mixin-deep": {
     "mixin-deep": {
       "version": "1.3.1",
       "version": "1.3.1",
@@ -7002,7 +7072,14 @@
     "mkdirp": {
     "mkdirp": {
       "version": "0.5.1",
       "version": "0.5.1",
       "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "from": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+      "dependencies": {
+        "minimist": {
+          "version": "0.0.8",
+          "from": "minimist@0.0.8",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+        }
+      }
     },
     },
     "mocha": {
     "mocha": {
       "version": "5.0.1",
       "version": "5.0.1",
@@ -7312,6 +7389,12 @@
           "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz",
           "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz",
           "dev": true
           "dev": true
         },
         },
+        "minimist": {
+          "version": "0.0.10",
+          "from": "minimist@>=0.0.1 <0.1.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+          "dev": true
+        },
         "optimist": {
         "optimist": {
           "version": "0.6.0",
           "version": "0.6.0",
           "from": "optimist@0.6.0",
           "from": "optimist@0.6.0",
@@ -8145,7 +8228,14 @@
     "optimist": {
     "optimist": {
       "version": "0.6.1",
       "version": "0.6.1",
       "from": "optimist@0.6.1",
       "from": "optimist@0.6.1",
-      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz"
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "dependencies": {
+        "minimist": {
+          "version": "0.0.10",
+          "from": "minimist@>=0.0.1 <0.1.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz"
+        }
+      }
     },
     },
     "optionator": {
     "optionator": {
       "version": "0.8.2",
       "version": "0.8.2",
@@ -8318,6 +8408,11 @@
       "from": "passport@>=0.3.2 <0.4.0",
       "from": "passport@>=0.3.2 <0.4.0",
       "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz"
       "resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz"
     },
     },
+    "passport-google-oauth20": {
+      "version": "1.0.0",
+      "from": "passport-google-oauth20@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-1.0.0.tgz"
+    },
     "passport-ldapauth": {
     "passport-ldapauth": {
       "version": "0.6.0",
       "version": "0.6.0",
       "from": "passport-ldapauth@>=0.6.0 <0.7.0",
       "from": "passport-ldapauth@>=0.6.0 <0.7.0",
@@ -8328,6 +8423,11 @@
       "from": "passport-local@>=1.0.0 <2.0.0",
       "from": "passport-local@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz"
       "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz"
     },
     },
+    "passport-oauth1": {
+      "version": "1.1.0",
+      "from": "passport-oauth1@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/passport-oauth1/-/passport-oauth1-1.1.0.tgz"
+    },
     "passport-oauth2": {
     "passport-oauth2": {
       "version": "1.4.0",
       "version": "1.4.0",
       "from": "passport-oauth2@>=1.4.0 <2.0.0",
       "from": "passport-oauth2@>=1.4.0 <2.0.0",
@@ -8338,6 +8438,11 @@
       "from": "passport-oauth2-refresh@>=1.0.0 <2.0.0",
       "from": "passport-oauth2-refresh@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-1.0.0.tgz"
       "resolved": "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-1.0.0.tgz"
     },
     },
+    "passport-orcid": {
+      "version": "0.0.3",
+      "from": "https://registry.npmjs.org/passport-orcid/-/passport-orcid-0.0.3.tgz",
+      "resolved": "https://registry.npmjs.org/passport-orcid/-/passport-orcid-0.0.3.tgz"
+    },
     "passport-saml": {
     "passport-saml": {
       "version": "0.15.0",
       "version": "0.15.0",
       "from": "passport-saml@>=0.15.0 <0.16.0",
       "from": "passport-saml@>=0.15.0 <0.16.0",
@@ -8372,6 +8477,11 @@
       "from": "passport-strategy@>=1.0.0 <2.0.0",
       "from": "passport-strategy@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz"
       "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz"
     },
     },
+    "passport-twitter": {
+      "version": "1.0.4",
+      "from": "https://registry.npmjs.org/passport-twitter/-/passport-twitter-1.0.4.tgz",
+      "resolved": "https://registry.npmjs.org/passport-twitter/-/passport-twitter-1.0.4.tgz"
+    },
     "path-browserify": {
     "path-browserify": {
       "version": "0.0.0",
       "version": "0.0.0",
       "from": "path-browserify@0.0.0",
       "from": "path-browserify@0.0.0",
@@ -12502,6 +12612,11 @@
       "from": "xtend@>=4.0.1 <5.0.0",
       "from": "xtend@>=4.0.1 <5.0.0",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
     },
     },
+    "xtraverse": {
+      "version": "0.1.0",
+      "from": "xtraverse@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/xtraverse/-/xtraverse-0.1.0.tgz"
+    },
     "y18n": {
     "y18n": {
       "version": "3.2.1",
       "version": "3.2.1",
       "from": "y18n@>=3.2.1 <4.0.0",
       "from": "y18n@>=3.2.1 <4.0.0",

+ 5 - 2
services/web/package.json

@@ -63,6 +63,7 @@
     "marked": "^0.3.5",
     "marked": "^0.3.5",
     "method-override": "^2.3.3",
     "method-override": "^2.3.3",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
+    "minimist": "1.2.0",
     "mocha": "^5.0.1",
     "mocha": "^5.0.1",
     "mongojs": "2.4.0",
     "mongojs": "2.4.0",
     "mongoose": "4.11.4",
     "mongoose": "4.11.4",
@@ -75,11 +76,14 @@
     "nvd3": "^1.8.6",
     "nvd3": "^1.8.6",
     "optimist": "0.6.1",
     "optimist": "0.6.1",
     "passport": "^0.3.2",
     "passport": "^0.3.2",
+    "passport-google-oauth20": "^1.0.0",
     "passport-ldapauth": "^0.6.0",
     "passport-ldapauth": "^0.6.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-oauth2": "^1.4.0",
     "passport-oauth2": "^1.4.0",
     "passport-oauth2-refresh": "^1.0.0",
     "passport-oauth2-refresh": "^1.0.0",
+    "passport-orcid": "0.0.3",
     "passport-saml": "^0.15.0",
     "passport-saml": "^0.15.0",
+    "passport-twitter": "^1.0.4",
     "pug": "^2.0.0-beta6",
     "pug": "^2.0.0-beta6",
     "react": "^15.4.2",
     "react": "^15.4.2",
     "react-dom": "^15.4.2",
     "react-dom": "^15.4.2",
@@ -99,8 +103,7 @@
     "v8-profiler": "^5.2.3",
     "v8-profiler": "^5.2.3",
     "valid-url": "^1.0.9",
     "valid-url": "^1.0.9",
     "xml2js": "0.2.0",
     "xml2js": "0.2.0",
-    "yauzl": "^2.8.0",
-    "minimist": "1.2.0"
+    "yauzl": "^2.8.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "autoprefixer": "^6.6.1",
     "autoprefixer": "^6.6.1",

BIN
services/web/public/apple-touch-icon.png


+ 15 - 0
services/web/public/atlassian-domain-verification.html

@@ -0,0 +1,15 @@
+<html>
+<body>
+<h1>Instructions</h1>
+<h2>What is this file?</h2>
+This file is used by Atlassian Cloud to verify ownership of your domain name.
+<br>
+Generated at 2017-02-02 11:42:45 UTC.
+
+<h1>Domain Verification Token</h1>
+This token must be found in the page at: <code>https://example.com/atlassian-domain-verification.html</code>
+<hr>
+<code id="domain-verification-token">6YyZjsahYa6Y5nyQDaNakzRPBJ6wVMQwN+2CDx-2wQtG/97OvOjc3gh4NCQ6Sgkv</code>
+
+</body>
+</html>

+ 1 - 0
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -516,6 +516,7 @@ define [
 
 
 				scope.$on '$destroy', () ->
 				scope.$on '$destroy', () ->
 					if scope.sharejsDoc?
 					if scope.sharejsDoc?
+						scope.$broadcast('changeEditor')
 						tearDownSpellCheck()
 						tearDownSpellCheck()
 						tearDownCursorPosition()
 						tearDownCursorPosition()
 						detachFromAce(scope.sharejsDoc)
 						detachFromAce(scope.sharejsDoc)

+ 7 - 3
services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee

@@ -3,12 +3,12 @@ define [], () ->
 		constructor: (@$scope, @adapter, @localStorage) ->
 		constructor: (@$scope, @adapter, @localStorage) ->
 			@$scope.$on 'editorInit', @jumpToPositionInNewDoc
 			@$scope.$on 'editorInit', @jumpToPositionInNewDoc
 
 
-			@$scope.$on 'beforeChangeDocument', () =>
-				@storeCursorPosition()
-				@storeFirstVisibleLine()
+			@$scope.$on 'beforeChangeDocument', @storePositionAndLine
 
 
 			@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
 			@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
 
 
+			@$scope.$on 'changeEditor', @storePositionAndLine
+
 			@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
 			@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
 				if line?
 				if line?
 					setTimeout () =>
 					setTimeout () =>
@@ -24,6 +24,10 @@ define [], () ->
 			@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
 			@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
 				@adapter.clearSelection()
 				@adapter.clearSelection()
 
 
+		storePositionAndLine: () =>
+			@storeCursorPosition()
+			@storeFirstVisibleLine()
+
 		jumpToPositionInNewDoc: () =>
 		jumpToPositionInNewDoc: () =>
 			@doc_id = @$scope.sharejsDoc?.doc_id
 			@doc_id = @$scope.sharejsDoc?.doc_id
 			setTimeout () =>
 			setTimeout () =>

+ 1 - 3
services/web/public/coffee/ide/share/controllers/ShareController.coffee

@@ -4,9 +4,7 @@ define [
 	App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking",
 	App.controller "ShareController", ["$scope", "$modal", "ide", "projectInvites", "projectMembers", "event_tracking",
 	($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
 	($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
 			$scope.openShareProjectModal = (isAdmin) ->
 			$scope.openShareProjectModal = (isAdmin) ->
-				if !isAdmin
-					return
-					
+				$scope.isAdmin = isAdmin;
 				event_tracking.sendMBOnce "ide-open-share-modal-once"
 				event_tracking.sendMBOnce "ide-open-share-modal-once"
 
 
 				$modal.open(
 				$modal.open(

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

@@ -24,7 +24,6 @@ define [
 	"main/affiliations/controllers/UserAffiliationsController"
 	"main/affiliations/controllers/UserAffiliationsController"
 	"main/affiliations/factories/UserAffiliationsDataService"
 	"main/affiliations/factories/UserAffiliationsDataService"
 	"main/keys"
 	"main/keys"
-	"main/account-merge-checker"
 	"main/cms/index"
 	"main/cms/index"
 	"analytics/AbTestingManager"
 	"analytics/AbTestingManager"
 	"directives/asyncForm"
 	"directives/asyncForm"

+ 0 - 5
services/web/public/coffee/main/account-merge-checker.coffee

@@ -1,5 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.controller "AccountMergeCheckerController", ($scope) ->
-		$scope.hasOlAccount = null

+ 2 - 2
services/web/public/coffee/main/contact-us.coffee

@@ -2,7 +2,7 @@ define [
 	"base"
 	"base"
 	"libs/platform"
 	"libs/platform"
 ], (App, platform) ->
 ], (App, platform) ->
-	App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
+	App.controller 'GroupPlanContactController', ($scope, $modal, $http) ->
 
 
 		$scope.form = {}
 		$scope.form = {}
 		$scope.sent = false
 		$scope.sent = false
@@ -20,7 +20,7 @@ define [
 				email: $scope.form.email
 				email: $scope.form.email
 				labels: "#{$scope.form.source} accounts"
 				labels: "#{$scope.form.source} accounts"
 				message: "Please contact me with more details"
 				message: "Please contact me with more details"
-				subject: "#{$scope.form.name} - General Enquiry - #{$scope.form.position} - #{$scope.form.university}"
+				subject: "#{$scope.form.name} - Group Enquiry - #{$scope.form.position} - #{$scope.form.university}"
 				inbox: "accounts"
 				inbox: "accounts"
 
 
 			request = $http.post "/support", data
 			request = $http.post "/support", data

+ 11 - 1
services/web/public/coffee/main/project-list/left-hand-menu-promo-controller.coffee

@@ -2,8 +2,18 @@ define [
 	"base"
 	"base"
 ], (App) ->
 ], (App) ->
 
 
-	App.controller 'LeftHandMenuPromoController', ($scope) ->
+	App.controller 'LeftHandMenuPromoController', ($scope, UserAffiliationsDataService) ->
 
 
 		$scope.hasProjects = window.data.projects.length > 0
 		$scope.hasProjects = window.data.projects.length > 0
 		$scope.userHasNoSubscription = window.userHasNoSubscription
 		$scope.userHasNoSubscription = window.userHasNoSubscription
 
 
+		_userHasNoAffiliation = () ->
+			$scope.userEmails = []
+			$scope.userAffiliations = []
+			UserAffiliationsDataService.getUserEmails().then (emails) ->
+				$scope.userEmails = emails
+				for email in emails
+					if email.affiliation
+						$scope.userAffiliations.push email.affiliation
+
+		_userHasNoAffiliation()

+ 1 - 0
services/web/public/google4f15e48c48709a75.html

@@ -0,0 +1 @@
+google-site-verification: google4f15e48c48709a75.html

+ 1 - 0
services/web/public/googleef256f97939bd9b7.html

@@ -0,0 +1 @@
+google-site-verification: googleef256f97939bd9b7.html

BIN
services/web/public/img/advocates/friend.jpeg


BIN
services/web/public/img/grid.png


BIN
services/web/public/img/homepage.png


BIN
services/web/public/img/homepage@2x.png


+ 1 - 0
services/web/public/img/other-brands/logo_google.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 510.2"><title>Shape-Copy</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path id="Shape-Copy" d="M254.85,303.57v-94.9H493.62C497.19,224.74,500,239.8,500,261c0,145.66-97.7,249.23-244.9,249.23C114.29,510.2,0,395.92,0,255.1S114.29,0,255.1,0C324,0,381.63,25.26,425.77,66.58L353.32,137c-18.37-17.35-50.51-37.76-98.21-37.76-84.44,0-153.32,70.15-153.32,156.12S170.66,411.48,255.1,411.48c97.7,0,133.67-67.6,140.31-107.65H254.85Z" fill="#fff"/></g></g></svg>

+ 1 - 0
services/web/public/img/other-brands/logo_google_alt.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 510.2"><title>Shape-Copy</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path id="Shape-Copy" d="M254.85,303.57v-94.9H493.62C497.19,224.74,500,239.8,500,261c0,145.66-97.7,249.23-244.9,249.23C114.29,510.2,0,395.92,0,255.1S114.29,0,255.1,0C324,0,381.63,25.26,425.77,66.58L353.32,137c-18.37-17.35-50.51-37.76-98.21-37.76-84.44,0-153.32,70.15-153.32,156.12S170.66,411.48,255.1,411.48c97.7,0,133.67-67.6,140.31-107.65H254.85Z" fill="#4885ED"/></g></g></svg>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
services/web/public/img/other-brands/logo_ieee.svg


+ 1 - 0
services/web/public/img/other-brands/logo_orcid.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M289.06,181.64H242.77V336.72h47.85c68.16,0,83.79-51.76,83.79-77.54C374.41,217.19,347.66,181.64,289.06,181.64Z" fill="#fff"/><path d="M250,0C111.91,0,0,111.91,0,250S111.91,500,250,500,500,388.09,500,250,388.09,0,250,0ZM168.55,249V363.67H138.48V154.49h30.08Zm-15-118.36a19.73,19.73,0,1,1,19.73-19.73A19.8,19.8,0,0,1,153.52,130.66Zm140.82,233.2H212.7V154.49h81.25c77.34,0,111.33,55.27,111.33,104.69C405.27,312.89,363.28,363.87,294.34,363.87Z" fill="#fff"/></g></g></svg>

+ 1 - 0
services/web/public/img/other-brands/logo_orcid_alt.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M289.06,181.64H242.77V336.72h47.85c68.16,0,83.79-51.76,83.79-77.54C374.41,217.19,347.66,181.64,289.06,181.64Z" fill="#95ba2e"/><path d="M250,0C111.91,0,0,111.91,0,250S111.91,500,250,500,500,388.09,500,250,388.09,0,250,0ZM168.55,249V363.67H138.48V154.49h30.08Zm-15-118.36a19.73,19.73,0,1,1,19.73-19.73A19.8,19.8,0,0,1,153.52,130.66Zm140.82,233.2H212.7V154.49h81.25c77.34,0,111.33,55.27,111.33,104.69C405.27,312.89,363.28,363.87,294.34,363.87Z" fill="#95ba2e"/></g></g></svg>

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
services/web/public/img/other-brands/logo_sharelatex.svg


+ 1 - 0
services/web/public/img/other-brands/logo_twitter.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 406.28"><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path d="M157.24,406.28c188.68,0,291.88-156.32,291.88-291.88,0-4.44,0-8.86-.3-13.26A208.72,208.72,0,0,0,500,48a204.76,204.76,0,0,1-58.92,16.14,102.94,102.94,0,0,0,45.1-56.74A205.58,205.58,0,0,1,421,32.34,102.68,102.68,0,0,0,246.22,125.9,291.24,291.24,0,0,1,34.8,18.72,102.66,102.66,0,0,0,66.56,155.66,101.82,101.82,0,0,1,20,142.82v1.3a102.62,102.62,0,0,0,82.3,100.56A102.42,102.42,0,0,1,56,246.44a102.7,102.7,0,0,0,95.84,71.24,205.84,205.84,0,0,1-127.4,44A208.82,208.82,0,0,1,0,360.2a290.42,290.42,0,0,0,157.24,46" fill="#fff"/></g></g></svg>

BIN
services/web/public/img/overleaf-partner/overleaf-greygreen-410.png


BIN
services/web/public/img/overleaf-partner/overleaf-white-410.png


BIN
services/web/public/static/brochures/Overleaf-Information-v8.pdf


Разница между файлами не показана из-за своего большого размера
+ 33 - 0
services/web/public/static/brochures/Overleaf-Institutional-Solutions-v1.pdf


BIN
services/web/public/static/brochures/Overleaf-Tri-Fold-leaflet.pdf


BIN
services/web/public/static/brochures/Overleaf-request-for-service.pdf


BIN
services/web/public/static/brochures/Overleaf_Institutional.pdf


BIN
services/web/public/static/campus-challenge/campus-challenge-2016-flyer.pdf


BIN
services/web/public/static/campus-challenge/campus-challenge-2016-poster.pdf


BIN
services/web/public/static/campus-challenge/campus-challenge-2017-flyer.pdf


BIN
services/web/public/static/campus-challenge/campus-challenge-2017-poster.pdf


BIN
services/web/public/static/latex/learn/free-online-introduction-to-latex-part-1.pdf


BIN
services/web/public/static/latex/learn/free-online-introduction-to-latex-part-2.pdf


BIN
services/web/public/static/latex/learn/free-online-introduction-to-latex-part-3.pdf


BIN
services/web/public/static/partners/caltech-library.gif


BIN
services/web/public/static/partners/stanford-building.jpg


+ 1 - 0
services/web/public/stylesheets/_ol_style_includes.less

@@ -2,3 +2,4 @@
 @import "app/front-chat-widget.less";
 @import "app/front-chat-widget.less";
 @import "app/ol-chat.less";
 @import "app/ol-chat.less";
 @import "app/templates-v2.less";
 @import "app/templates-v2.less";
+@import "app/login-register.less";

+ 13 - 1
services/web/public/stylesheets/app/blog-posts.less

@@ -32,7 +32,7 @@
     li {
     li {
       display: inline-block;
       display: inline-block;
       margin: 0;
       margin: 0;
-      padding: 0 @margin-sm 0 0;
+      padding: 0 @padding-sm @padding-sm 0;
     }
     }
     a {
     a {
       font-size: small;
       font-size: small;
@@ -44,4 +44,16 @@
     border-radius: @border-radius-base;
     border-radius: @border-radius-base;
     padding: @padding-sm;
     padding: @padding-sm;
   }
   }
+
+  .figure {
+    background-color: #ffffff;
+    border: 1px solid @ol-blue-gray-2;
+    display: inline-block;
+    margin: 0 auto @margin-sm 0;
+    padding: @padding-sm;
+    .figure-caption {
+      padding-top: @padding-sm;
+      font-size: small;
+    }
+  }
 }
 }

+ 57 - 0
services/web/public/stylesheets/app/cms-page.less

@@ -4,6 +4,10 @@
   including About and Blog
   including About and Blog
 */
 */
 .cms-page {
 .cms-page {
+  img {
+    height: auto;
+    max-width: 100%;
+  }
   .btn-description {
   .btn-description {
     margin-right: @margin-sm;
     margin-right: @margin-sm;
   }
   }
@@ -85,4 +89,57 @@
       margin-bottom: @margin-sm;
       margin-bottom: @margin-sm;
     }
     }
   }
   }
+
+  /*
+    universities page
+  */
+  #universities-container {
+    display: table;
+    padding: @padding-md;
+    width: 100%;
+    .row {
+      display: table-row;
+      div {
+        border-bottom: 1px solid @gray-lightest;
+        display: table-cell;
+        float: none;
+        padding: @padding-md;
+        vertical-align: middle;
+      }
+    }
+    .row:first-child {
+      // parent container contains padding
+      div {
+        padding-top: 0;
+      }
+    }
+    .row:last-child {
+      // parent container contains padding
+      div {
+        border: 0;
+        padding-bottom: 0;
+      }
+    }
+    p {
+      margin: 0 auto;
+      width: 100%;
+    }
+    // Logos
+    .uni-logo {
+      margin: 0 auto;
+      max-height: 55px;
+      min-width: 55px;
+    }
+    .university-claim-btn {
+      text-align: center;
+    }
+  }
+
+  /*
+    videos
+  */
+  video {
+    height: auto;
+    max-width: 100%;
+  }
 }
 }

+ 9 - 0
services/web/public/stylesheets/app/content_page.less

@@ -0,0 +1,9 @@
+/*
+  Styling for content pages
+  Including: about, home, blog, /for/__, legal, contact, portals
+*/
+.content-page {
+  a:not(.btn) {
+    color: @link-color-alt;
+  }
+}

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

@@ -30,6 +30,13 @@
 		}
 		}
 	}
 	}
 
 
+	.public-access-level.public-access-level--notice {
+		background-color: @gray-lightest;
+		border-bottom: none;
+		margin-top: @margin-md;
+		padding-top: @margin-md;
+	}
+
 	.project-member, .project-invite {
 	.project-member, .project-invite {
 		&:hover {
 		&:hover {
 			background-color: @gray-lightest;
 			background-color: @gray-lightest;

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

@@ -72,17 +72,6 @@
 			background-color: @toolbar-btn-active-bg-color;
 			background-color: @toolbar-btn-active-bg-color;
 			box-shadow: @toolbar-btn-active-shadow;
 			box-shadow: @toolbar-btn-active-shadow;
 		}
 		}
-		&.btn-full-height-disabled {
-			opacity: 0.65;
-			&:hover,
-			&.active,
-			&:active {
-				text-shadow: none;
-				background-color: transparent;
-				color: @toolbar-btn-color;
-				box-shadow: none;
-			}
-		}
 		.label {
 		.label {
 			top: 4px;
 			top: 4px;
 			right: 4px;
 			right: 4px;

+ 0 - 3
services/web/public/stylesheets/app/features.less

@@ -13,9 +13,6 @@
 	h3 {
 	h3 {
 		margin: 0;
 		margin: 0;
 	}
 	}
-	p {
-		color: @gray
-	}
 	i {
 	i {
 		color: lighten(@blue, 15%);
 		color: lighten(@blue, 15%);
 	}
 	}

+ 86 - 31
services/web/public/stylesheets/app/homepage.less

@@ -1,3 +1,5 @@
+@register-v-spacing: 20px;
+
 .deprecated-sl-masthead {
 .deprecated-sl-masthead {
 	display: inline-block;
 	display: inline-block;
 	margin-top: @header-height;
 	margin-top: @header-height;
@@ -38,15 +40,12 @@
 }
 }
 
 
 .masthead {
 .masthead {
-	display: inline-block;
-	width: 100%;
-	background-color: rgba(0,0,0,0.85);
-	background-image: -webkit-linear-gradient(left,rgba(153,93,179,0.6),rgba(44,155,219,0.6));
-	background-image: linear-gradient(to right,rgba(153,93,179,0.6),rgba(44,155,219,0.6));
+	background-image: -webkit-linear-gradient(to left, rgba(79,156,69,1.0), rgba(28,91,38,1.0));
+	background-image: linear-gradient(to left, rgba(79,156,69,1.0), rgba(28,91,38,1.0));
 	position: relative;
 	position: relative;
 	text-align: center;
 	text-align: center;
 	overflow: hidden;
 	overflow: hidden;
-	margin-top: @header-height;
+	padding-top: @header-height;
 	h1, p, label {
 	h1, p, label {
 		color: white;
 		color: white;
 		text-align: center;
 		text-align: center;
@@ -70,10 +69,13 @@
 		text-rendering: auto;
 		text-rendering: auto;
 		margin-bottom: @line-height-computed;
 		margin-bottom: @line-height-computed;
 	}
 	}
+	label {
+		display: block;
+	}
 	.register-banner {
 	.register-banner {
 		background-image: -webkit-linear-gradient(top,rgba(0,0,0,0.7),rgba(0,0,0,0.9));
 		background-image: -webkit-linear-gradient(top,rgba(0,0,0,0.7),rgba(0,0,0,0.9));
 		background-image: linear-gradient(to bottom,rgba(0,0,0,0.7),rgba(0,0,0,0.9));
 		background-image: linear-gradient(to bottom,rgba(0,0,0,0.7),rgba(0,0,0,0.9));
-		padding: @line-height-computed 0;
+		padding: @register-v-spacing 0;
 		position: absolute;
 		position: absolute;
 		bottom: 0;
 		bottom: 0;
 		width: 100%;
 		width: 100%;
@@ -83,13 +85,17 @@
 			font-family: @font-family-sans-serif;
 			font-family: @font-family-sans-serif;
 			font-weight: 500;
 			font-weight: 500;
 			letter-spacing: 1px;
 			letter-spacing: 1px;
+			margin-bottom: @register-v-spacing;
 		}
 		}
 		.form-group {
 		.form-group {
 			margin-left: @line-height-computed / 2;
 			margin-left: @line-height-computed / 2;
 		}
 		}
+		.input-lg {
+			border-radius: 9999px;
+		}
 	}
 	}
 	.screenshot {
 	.screenshot {
-		height: 550px;
+		height: 600px;
 		margin: auto;
 		margin: auto;
 		margin-bottom: -50px;
 		margin-bottom: -50px;
 		overflow-y: hidden;
 		overflow-y: hidden;
@@ -97,7 +103,7 @@
 		max-width: 960px;
 		max-width: 960px;
 		.img {
 		.img {
 			max-width: 960px;
 			max-width: 960px;
-			background-image: url('/img/screen.png');
+			background-image: url('/img/homepage.png');
 			background-size: 100%;
 			background-size: 100%;
 			background-repeat: no-repeat;
 			background-repeat: no-repeat;
 			margin: auto;
 			margin: auto;
@@ -111,38 +117,75 @@
 		only screen and (                min-resolution: 192dpi),
 		only screen and (                min-resolution: 192dpi),
 		only screen and (                min-resolution: 2dppx) { 
 		only screen and (                min-resolution: 2dppx) { 
 			.img {
 			.img {
-				background-image: url('/img/screen@2x.png');
+				background-image: url('/img/homepage@2x.png');
 			}
 			}
 		}
 		}
 	}
 	}
-	.btn-hero {
-		background-color: transparent;
-		border-color: white;
-		border-width: 3px;
-		color: white;
-		margin-right: @line-height-computed / 2;
-		&:hover {
-			background-color: white;
-			color: @gray-dark;
+}
+
+.hp-register-external-separator {
+	margin: 0 0 (@register-v-spacing / 2);
+	color: #FFF;
+}
+
+	.hp-register-external-separator-or {
+		vertical-align: middle;
+		&::before,
+		&::after {
+			content: "";
+			display: inline-block;
+			vertical-align: middle;
+			width: 5em;
+			height: 1px;
+			background-color: rgba(255, 255, 255, .3);
 		}
 		}
-		&.btn-primary {
-			border-color: lighten(@link-color, 10%);
-			color: lighten(@link-color, 10%);
-			&:hover {
-				background-color: lighten(@link-color, 10%);
-				color: white;
-			}
+		&::before {
+			margin-right: 1.25em;
+		}
+		&::after {
+			margin-left: 1.25em;
+		}
+	}
+
+.hp-register-newsletter-checkbox {
+	font-size: 90%;
+	& > .checkbox-newsletter {
+		text-align: left;
+		@media only screen and (min-width: @screen-sm-min) {
+			text-align: center;
 		}
 		}
 	}
 	}
 }
 }
 
 
+.hp-login-btn {
+	min-width: 220px;
+}
+
+.hp-register-form-email-pwd {
+	position: relative;
+}
+
+	.hp-register-form-email-pwd-btn-container.form-group {
+		display: block;
+		margin-top: (@register-v-spacing / 2);
+		@media only screen and (min-width: @screen-md-min) {
+			position: absolute;
+			display: inline-block;
+			height: 100%;
+			top: -(@register-v-spacing / 2);
+			& > .btn-hero {
+				height: 100%;
+			}
+		}
+	}
+
 .universities {
 .universities {
-	padding-bottom: @line-height-computed;
+	border-bottom: 1px solid @gray-lighter;
 	text-align: center;
 	text-align: center;
-	margin: 0 auto;
-	img {
-		max-width: 100%;
-		max-height: 60px;
+	.uni-logo {
+		display: inline-block;
+		padding: 0 @padding-md;
+		width: 20%;
 	}
 	}
 }
 }
 
 
@@ -152,6 +195,12 @@
 	border-bottom: 1px solid @gray-lightest;
 	border-bottom: 1px solid @gray-lightest;
 }
 }
 
 
+.pattern-grid {
+	background: url('/img/grid.png') repeat @content-alt-bg-color;
+	border-top: 1px solid @gray-lighter;
+	border-bottom: 1px solid @gray-lighter;
+}
+
 .real-time-example {
 .real-time-example {
 	.cursor {
 	.cursor {
 		background-color: hsl(200, 70%, 70%);
 		background-color: hsl(200, 70%, 70%);
@@ -212,3 +261,9 @@
 	}
 	}
 	box-shadow: 0 3px 5px rgba(0,0,0,.3);
 	box-shadow: 0 3px 5px rgba(0,0,0,.3);
 }
 }
+
+@media only screen and (max-width: @screen-sm-max) {
+	.doc-history-example {
+		margin-bottom: @margin-md;
+	}
+}

+ 195 - 0
services/web/public/stylesheets/app/login-register.less

@@ -0,0 +1,195 @@
+@brand-ieee-color       : #00629B;
+@brand-google-color     : #4885ED;
+@brand-twitter-color    : #1DA1F2;
+@brand-orcid-color      : #A6CE39;
+@brand-sharelatex-color : #A93529;
+
+.make-login-register-brand-btn(@bg-color) {
+	background-color: @bg-color;
+	text-indent: -10px;
+	padding-left: 0;
+	padding-right: 0;
+	&:focus,
+	&:hover {
+		background-color: darken(@bg-color, 8%);
+	}
+	&:active {
+		background-color: darken(@bg-color, 16%);
+	} 
+}
+
+.make-login-register-brand-btn-alt(@color) {
+	background-color: #FFF;
+	color: @color;
+	text-indent: -10px;
+	padding-left: 0;
+	padding-right: 0;
+	&:focus,
+	&:hover {
+		background-color: #FFF;
+		color: darken(@color, 8%);
+	}
+	&:active {
+		background-color: #FFF;
+		color: darken(@color, 16%);
+	} 
+}
+
+.login-register-container {
+	max-width: 400px;
+	margin: 0 auto;
+}
+	.login-register-header {
+		padding-top: @line-height-computed;
+		padding-bottom: @line-height-computed - 5;
+		border-bottom: solid 1px @hr-border;
+	}
+		.login-register-header-heading {
+			margin: 0;
+			color: @text-color;
+		}
+
+	.login-register-card {
+		padding-top: 0;
+		padding-bottom: 0;
+		text-align: center;
+	}
+
+		.login-register-form,
+		.login-register-sharelatex {
+			padding: @line-height-computed;
+			border-bottom: solid 1px @hr-border;
+			&:last-child {
+				border-bottom-width: 0;
+			}
+		}
+		.login-register-other-links {
+			padding: @line-height-computed;
+		}
+
+		.login-register-text,
+		.login-register-hr-text-container {
+			margin: 0;
+			line-height: 1;
+			font-size: 90%;
+		}
+
+		.login-register-text {
+			padding-bottom: 25px;
+			&:last-child {
+				padding-bottom: 0;
+			}
+		}
+
+		.login-register-hr-text-container {
+			position: relative;
+			padding: @line-height-computed 0;
+
+			&::before {
+				content: '';
+				position: absolute;
+				height: 1px;
+				background-color: @hr-border;
+				top: 50%;
+				left: 0;
+				right: 0;
+			}
+		}
+			.login-register-hr-text {
+				position: relative;
+				background-color: #FFF;
+				padding: 0 (@line-height-computed / 2);
+			}
+		.login-register-sharelatex-tooltip {
+			display: inline-block;
+			font-size: 135%;
+			position: relative;
+			top: 2px;
+			margin-left: 3px;
+			color: @link-color;
+			cursor: pointer;
+		}
+
+		.login-register-newsletter {
+			font-size: 90%;
+			margin-top: @line-height-computed;
+			text-align: left;
+
+			& > .checkbox {
+				margin-bottom: 0;
+			}
+		}
+
+		.login-btn-ieee {
+			.make-login-register-brand-btn(@brand-ieee-color);
+		}
+
+		.login-btn-google {
+			.make-login-register-brand-btn(@brand-google-color);
+		} 
+
+		.login-btn-google-alt {
+			.make-login-register-brand-btn-alt(@brand-google-color);
+		} 
+
+		.login-btn-twitter {
+			.make-login-register-brand-btn(@brand-twitter-color);
+		} 
+
+		.login-btn-orcid {
+			.make-login-register-brand-btn(@brand-orcid-color);
+		} 
+
+		.login-btn-orcid-alt {
+			.make-login-register-brand-btn-alt(darken(@brand-orcid-color, 6%));
+		} 
+
+		.login-btn-sharelatex {
+			.make-login-register-brand-btn(@brand-sharelatex-color);
+		} 
+			.login-btn-icon {
+				display: inline-block;
+				float: left;
+				margin-left: 5px;
+				background: url(/img/brand/lion.svg) center/contain no-repeat;
+				width: 24px;
+
+				&::before {
+					content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome.
+				}
+			}
+			.login-btn-icon-ieee {
+				background-image: url(/img/other-brands/logo_ieee.svg);
+			}
+			.login-btn-icon-google {
+				background-image: url(/img/other-brands/logo_google.svg);
+			}
+			.login-btn-icon-google-alt {
+				background-image: url(/img/other-brands/logo_google_alt.svg);
+			}
+			.login-btn-icon-twitter {
+				background-image: url(/img/other-brands/logo_twitter.svg);
+			}
+			.login-btn-icon-orcid {
+				background-image: url(/img/other-brands/logo_orcid.svg);
+			}
+			.login-btn-icon-orcid-alt {
+				background-image: url(/img/other-brands/logo_orcid_alt.svg);
+			}
+			.login-btn-icon-sharelatex {
+				background-image: url(/img/other-brands/logo_sharelatex.svg);
+			}
+
+		.login-register-header-heading {
+			margin: 0;
+			color: @text-color;
+		}
+
+.registration-message-heading {
+	color: @text-color;	
+}
+
+.registration-message-details {
+	font-size: 90%;
+}
+

+ 0 - 10
services/web/public/stylesheets/app/plans.less

@@ -77,17 +77,7 @@
 		}
 		}
 	}
 	}
 	.circle-img {
 	.circle-img {
-		border-radius: 50%;
 		float: right;
 		float: right;
-		height: 100px;
-		overflow: hidden;
-		position: relative;
-		width: 100px;
-		img {
-			display: inline;
-			margin: 0 auto;
-			width: 100%;
-		}
 	}
 	}
 	.faq {
 	.faq {
 		p {
 		p {

+ 0 - 42
services/web/public/stylesheets/app/portals.less

@@ -100,51 +100,9 @@
   }
   }
   // End Print
   // End Print
 
 
-  /*
-    Begin Tabs
-  */
   .nav-tabs {
   .nav-tabs {
-    // Overrides for nav.less
     background-color: @ol-blue-gray-0;
     background-color: @ol-blue-gray-0;
-    border: 0!important;
-    margin-bottom: @margin-md;
-    margin-top: -@line-height-computed; //- adjusted for portal-name
-    padding: @padding-lg 0 @padding-md;
-    text-align: center;
-
-    a {
-      color: @link-color;
-      &:hover {
-        background-color: transparent!important;
-        border: 0!important;
-        color: @link-hover-color!important;
-      }
-    }
-
-    li {
-      display: inline-block;
-      float: none;
-      a {
-        border: 0;
-      }
-    }
-
-    li.active > a {
-      background-color: transparent!important;
-      border: 0;
-      border-bottom: 1px solid @accent-color-secondary!important;
-      color: @accent-color-secondary;
-      &:hover {
-        color: @accent-color-secondary!important;
-      }
-    }
-  }
-
-  .tab-content:extend(.container) {
-    background-color: transparent!important;
-    border: none!important;
   }
   }
-  // End Tabs
 
 
   @media (max-width: @screen-size-sm-max) {
   @media (max-width: @screen-size-sm-max) {
     .content-pull {
     .content-pull {

+ 35 - 54
services/web/public/stylesheets/app/templates-v2.less

@@ -16,100 +16,81 @@
 }
 }
 
 
 .cta-links {
 .cta-links {
-	margin-top: @margin-lg;
+	margin-bottom: @margin-md;
 
 
 	.cta-link.btn {
 	.cta-link.btn {
-		margin: 0 @margin-sm;
+		margin-right: @margin-sm;
 	}
 	}
 }
 }
 
 
 .tag-link {
 .tag-link {
-	margin-right: @margin-xs;
+	margin: 0 @margin-xs @margin-sm 0;
+}
+
+.templates-container {
+	column-count: 3;
+	column-gap: 1em;
 }
 }
 
 
 .template-thumbnail {
 .template-thumbnail {
+	&.template-thumbnail__container {
+		display: inline-block;
+		margin: 0 0 1em;
+		width: 100%;
+	}
 
 
 	.thumbnail {
 	.thumbnail {
 		box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 		box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-		-webkit-box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);
-		height: 465px;
 		margin: 5% 0;
 		margin: 5% 0;
-
-		&.thumbnail-tag {
-			height: 100px;
-		}
-
-		img {
-			width: 100%;
-			height: 100%;
-		}
-	}
-
-	a {
 		padding:0px;
 		padding:0px;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		overflow: hidden;
+
 		h3 {
 		h3 {
 			color:@link-color;
 			color:@link-color;
 			margin: 10px 0px 10px 20px;
 			margin: 10px 0px 10px 20px;
 		}
 		}
 
 
-		display: flex;
-		justify-content: center;
-		align-items: center;
+		&.thumbnail-tag {
+			height: 100px;
+		}
 	}
 	}
 
 
 	.caption {
 	.caption {
-        // Override ShareLatex template styles
-        background: none;
-        border: none;
-
+		// Override ShareLatex template styles
+		background: none;
+		border: none;
 		text-align: center;
 		text-align: center;
+	}
 
 
-		.description {
-			font-style: italic;
-			padding: 5px 0;
-			text-overflow: ellipsis;
-			overflow: hidden;
-			white-space: nowrap;
-		}
+	.caption__description {
+		font-style: italic;
+		padding: 5px 0;
+		.text-overflow();
+	}
 
 
+	.caption__title {
+		display: inline-block;
+		max-width: 100%;
+		.text-overflow();
 	}
 	}
 
 
 	/* Media Queries */
 	/* Media Queries */
 	@media (max-width: @screen-md-min) {
 	@media (max-width: @screen-md-min) {
 		.thumbnail {
 		.thumbnail {
-			height: 400px;
-			width: 300px;
-
 			margin: 5% auto;
 			margin: 5% auto;
 		}
 		}
 
 
-		img {
-			width: 90%;
-		}
-
 		.caption .description {
 		.caption .description {
 			padding: 5px 50px;
 			padding: 5px 50px;
 		}
 		}
 	}
 	}
-
 }
 }
-
-    .section-preview {
-        margin-top: @margin-xl;
-
-        .abstract {
-            padding-right:10%;
-            padding-bottom: 10%;
-
-            @media (max-width: @screen-md-min) {
-                padding: 5%;
-            }
-        }
-    }
-
     .section-tags {
     .section-tags {
 		margin-bottom: @margin-xl;
 		margin-bottom: @margin-xl;
-		margin-top: @margin-xl;
+		margin-top: @margin-md;
 	}
 	}
 
 
 	.template-large-pdf-preview {
 	.template-large-pdf-preview {

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

@@ -15,6 +15,7 @@
 	}
 	}
 
 
 	img {
 	img {
+		height: auto;
 		max-width: 100%;
 		max-width: 100%;
 	}
 	}
 	
 	

+ 3 - 0
services/web/public/stylesheets/components/alerts.less

@@ -65,6 +65,9 @@
 .alert-danger {
 .alert-danger {
   .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
   .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);
 }
 }
+.alert-alt {
+  .alert-variant(@alert-alt-bg; @alert-alt-border; @alert-alt-text);
+}
 
 
 .alert when (@is-overleaf = true) {
 .alert when (@is-overleaf = true) {
   a {
   a {

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

@@ -122,6 +122,10 @@
 // Button Sizes
 // Button Sizes
 // --------------------------------------------------
 // --------------------------------------------------
 
 
+.btn-xl {
+  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-h2; @line-height-large; @btn-border-radius-large);
+}
+
 .btn-lg {
 .btn-lg {
   // line-height: ensure even-numbered height of button next to large input
   // line-height: ensure even-numbered height of button next to large input
   .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large);
   .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large);

+ 1 - 1
services/web/public/stylesheets/components/icons.less

@@ -1,6 +1,6 @@
 // Colors
 // Colors
 .icon-accent {
 .icon-accent {
-  color: @accent-color-secondary;
+  color: @accent-color-secondary!important;
 }
 }
 
 
 // Sizes
 // Sizes

+ 17 - 0
services/web/public/stylesheets/components/images.less

@@ -0,0 +1,17 @@
+.circle-img {
+	border-radius: 50%;
+	float: left;
+	// float: right;
+	height: 100px;
+	overflow: hidden;
+	position: relative;
+	max-width: 100px;
+	img {
+		display: inline;
+		margin: 0 auto;
+		width: 100%;
+	}
+}
+.blockquote-with-img {
+	margin-left: 115px;
+}

+ 44 - 0
services/web/public/stylesheets/components/tabs.less

@@ -0,0 +1,44 @@
+.ol-tabs {
+  // Overrides for nav.less
+  .nav-tabs {
+    border: 0!important;
+    margin-bottom: @margin-md;
+    margin-top: -@line-height-computed; //- adjusted for portal-name
+    padding: @padding-lg 0 @padding-md;
+    text-align: center;
+
+    a {
+      color: @link-color;
+      &:hover {
+        background-color: transparent!important;
+        border: 0!important;
+        color: @link-hover-color!important;
+      }
+    }
+  }
+
+  li {
+    display: inline-block;
+    float: none;
+    a {
+      border: 0;
+    }
+  }
+
+  li.active > a {
+    background-color: transparent!important;
+    border: 0!important;
+    border-bottom: 1px solid @accent-color-secondary!important;
+    color: @accent-color-secondary!important;
+    &:hover {
+      border-bottom: 1px solid @accent-color-secondary!important;
+      color: @accent-color-secondary!important;
+    }
+  }
+  .tab-content:extend(.container) {
+    background-color: transparent!important;
+    border: none!important;
+  }
+}
+
+  

+ 9 - 1
services/web/public/stylesheets/core/_common-variables.less

@@ -17,7 +17,6 @@
 //** Link hover color set via `darken()` function.
 //** Link hover color set via `darken()` function.
 @link-hover-color:      darken(@link-color, 15%);
 @link-hover-color:      darken(@link-color, 15%);
 
 
-
 //== Typography
 //== Typography
 //
 //
 //## Font, line-height, and color for body text, headings, and more.
 //## Font, line-height, and color for body text, headings, and more.
@@ -82,6 +81,11 @@
 @padding-xs-vertical:       1px;
 @padding-xs-vertical:       1px;
 @padding-xs-horizontal:     5px;
 @padding-xs-horizontal:     5px;
 
 
+@padding-sm: 10px;
+@padding-md: 20px;
+@padding-lg: 30px;
+@padding-xl: 40px;
+
 @line-height-large:         1.33;
 @line-height-large:         1.33;
 @line-height-small:         1.5;
 @line-height-small:         1.5;
 
 
@@ -586,6 +590,10 @@
 @alert-danger-text:           @state-danger-text;
 @alert-danger-text:           @state-danger-text;
 @alert-danger-border:         @state-danger-border;
 @alert-danger-border:         @state-danger-border;
 
 
+@alert-alt-bg:               @state-info-bg;
+@alert-alt-text:             @state-info-text;
+@alert-alt-border:           @state-info-border;
+
 
 
 //== Progress bars
 //== Progress bars
 //
 //

+ 6 - 5
services/web/public/stylesheets/core/ol-variables.less

@@ -49,8 +49,10 @@
 @text-small-color       : @ol-type-color;
 @text-small-color       : @ol-type-color;
 @text-color             : @ol-type-color;
 @text-color             : @ol-type-color;
 @link-color             : @ol-blue;
 @link-color             : @ol-blue;
+@link-color-alt         : @ol-green;
 @link-active-color      : @ol-dark-green;
 @link-active-color      : @ol-dark-green;
 @link-hover-color       : @ol-dark-blue;
 @link-hover-color       : @ol-dark-blue;
+@hr-border              : @ol-blue-gray-1;
 
 
 // Button colors and sizing
 // Button colors and sizing
 @btn-border-width        : 0;
 @btn-border-width        : 0;
@@ -82,11 +84,6 @@
 // Padding
 // Padding
 @padding-xs-horizontal   : 8px;
 @padding-xs-horizontal   : 8px;
 
 
-@padding-sm: 10px;
-@padding-md: 20px;
-@padding-lg: 30px;
-@padding-xl: 40px;
-
 // Alerts
 // Alerts
 @alert-padding : 15px;
 @alert-padding : 15px;
 @alert-border-radius : @border-radius-base;
 @alert-border-radius : @border-radius-base;
@@ -108,6 +105,10 @@
 @alert-danger-text   : #FFF;
 @alert-danger-text   : #FFF;
 @alert-danger-border : transparent;
 @alert-danger-border : transparent;
 
 
+@alert-alt-bg    : @ol-blue-gray-1;
+@alert-alt-text  : @ol-type-color;
+@alert-alt-border: transparent;
+
 
 
 // Tags
 // Tags
 @tag-border-radius       : 9999px;
 @tag-border-radius       : 9999px;

+ 3 - 0
services/web/public/stylesheets/ol-style.less

@@ -8,12 +8,15 @@
 @import "_ol_style_includes.less";
 @import "_ol_style_includes.less";
 @import "components/embed-responsive.less";
 @import "components/embed-responsive.less";
 @import "components/icons.less";
 @import "components/icons.less";
+@import "components/images.less";
 @import "components/navs-ol.less";
 @import "components/navs-ol.less";
 @import "components/pagination.less";
 @import "components/pagination.less";
+@import "components/tabs.less";
 
 
 // Pages
 // Pages
 @import "app/about.less";
 @import "app/about.less";
 @import "app/blog-posts.less";
 @import "app/blog-posts.less";
 @import "app/cms-page.less";
 @import "app/cms-page.less";
+@import "app/content_page.less";
 @import "app/plans-ol.less";
 @import "app/plans-ol.less";
 @import "app/portals.less";
 @import "app/portals.less";

+ 13 - 1
services/web/test/acceptance/coffee/ExportsTests.coffee

@@ -30,7 +30,13 @@ describe 'Exports', ->
 			@owner.request {
 			@owner.request {
 				method: 'POST',
 				method: 'POST',
 				url: "/project/#{@project_id}/export/#{@brand_variation_id}",
 				url: "/project/#{@project_id}/export/#{@brand_variation_id}",
-				json: {},
+				json: true,
+				body:
+					title: 'title'
+					description: 'description'
+					author: 'author'
+					license: 'other'
+					show_source: true
 			}, (error, response, body) =>
 			}, (error, response, body) =>
 				throw error if error?
 				throw error if error?
 				expect(response.statusCode).to.equal 200
 				expect(response.statusCode).to.equal 200
@@ -42,6 +48,12 @@ describe 'Exports', ->
 			# project details should match
 			# project details should match
 			expect(project.id).to.equal @project_id
 			expect(project.id).to.equal @project_id
 			expect(project.rootDocPath).to.equal '/main.tex'
 			expect(project.rootDocPath).to.equal '/main.tex'
+			# gallery details should match
+			expect(project.metadata.title).to.equal 'title'
+			expect(project.metadata.description).to.equal 'description'
+			expect(project.metadata.author).to.equal 'author'
+			expect(project.metadata.license).to.equal 'other'
+			expect(project.metadata.show_source).to.equal true
 			# version should match what was retrieved from project-history
 			# version should match what was retrieved from project-history
 			expect(project.historyVersion).to.equal @version
 			expect(project.historyVersion).to.equal @version
 			# user details should match
 			# user details should match

+ 34 - 0
services/web/test/acceptance/coffee/RedirectUrlsTests.coffee

@@ -0,0 +1,34 @@
+should = require('chai').should()
+assert = require('chai').assert
+async = require("async")
+request = require "./helpers/request"
+MockV1Api = require "./helpers/MockV1Api"
+
+assertRedirect = (method, path, expectedStatusCode, destination, cb) ->
+	request[method] path, (error, response) ->
+		should.not.exist error
+		response.statusCode.should.equal expectedStatusCode
+		response.headers.location.should.equal destination
+		cb()
+
+describe "RedirectUrls", ->
+	before ->
+		@timeout(1000)
+
+	it 'proxy static URLs', (done) ->
+		assertRedirect 'get', '/redirect/one', 302, '/destination/one', done
+
+	it 'proxy dynamic URLs', (done) ->
+		assertRedirect 'get', '/redirect/params/42', 302, '/destination/42/params', done
+
+	it 'proxy URLs with baseUrl', (done) ->
+		assertRedirect 'get', '/redirect/base_url', 302, 'https://example.com/destination/base_url', done
+
+	it 'proxy URLs with POST with a 307', (done) ->
+		assertRedirect 'post', '/redirect/get_and_post', 307, '/destination/get_and_post', done
+
+	it 'proxy URLs with multiple support methods', (done) ->
+		assertRedirect 'get', '/redirect/get_and_post', 302, '/destination/get_and_post', done
+
+	it 'redirects with query params', (done) ->
+		assertRedirect 'get', '/redirect/qs?foo=bar&baz[]=qux1&baz[]=qux2', 302, '/destination/qs?foo=bar&baz[]=qux1&baz[]=qux2', done

+ 10 - 0
services/web/test/acceptance/coffee/TokenAccessTests.coffee

@@ -415,3 +415,13 @@ describe 'TokenAccess', ->
 			try_content_access(@other2, @project_id, (response, body) =>
 			try_content_access(@other2, @project_id, (response, body) =>
 				expect(body.privilegeLevel).to.equal false
 				expect(body.privilegeLevel).to.equal false
 			, done)
 			, done)
+
+	describe 'unimported v1 project', ->
+		it 'should redirect to v1', (done) ->
+			unimportedV1Token = '123abc'
+			try_read_and_write_token_access(@owner, unimportedV1Token, (response, body) =>
+				expect(response.statusCode).to.equal 302
+				expect(response.headers.location).to.equal(
+					'http://overleaf.test:5000/123abc'
+				)
+			, done)

+ 1 - 1
services/web/test/acceptance/coffee/helpers/User.coffee

@@ -58,7 +58,7 @@ class User
 			@getCsrfToken (error) =>
 			@getCsrfToken (error) =>
 				return callback(error) if error?
 				return callback(error) if error?
 				@request.post {
 				@request.post {
-					url: "/login"
+					url: if settings.enableLegacyLogin then "/login/legacy" else "/login"
 					json: { email, @password }
 					json: { email, @password }
 				}, callback
 				}, callback
 
 

+ 23 - 0
services/web/test/acceptance/config/settings.test.coffee

@@ -3,6 +3,7 @@ v1Api =
 
 
 module.exports =
 module.exports =
 	enableSubscriptions: true
 	enableSubscriptions: true
+	enableLegacyRegistration: true
 
 
 	features: features =
 	features: features =
 		v1_free:
 		v1_free:
@@ -105,3 +106,25 @@ module.exports =
 			path: (params) -> "/universities/list/#{params.id}"
 			path: (params) -> "/universities/list/#{params.id}"
 		'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
 		'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
 		'/proxy/missing/baseUrl': path: '/foo/bar'
 		'/proxy/missing/baseUrl': path: '/foo/bar'
+		'/proxy/get_and_post': {
+			methods: ['get', 'post'],
+			path: '/destination/get_and_post'
+		}
+
+	overleaf:
+		host: "http://overleaf.test:5000"
+
+	redirects:
+		'/redirect/one': '/destination/one',
+		'/redirect/get_and_post': {
+			methods: ['get', 'post'],
+			url: '/destination/get_and_post'
+		},
+		'/redirect/base_url': {
+			baseUrl: 'https://example.com'
+			url: '/destination/base_url'
+		},
+		'/redirect/params/:id': {
+			url: (params) -> "/destination/#{params.id}/params"
+		},
+		'/redirect/qs': '/destination/qs'

+ 1 - 1
services/web/test/unit/coffee/Authentication/AuthenticationControllerTests.coffee

@@ -15,7 +15,7 @@ describe "AuthenticationController", ->
 		tk.freeze(Date.now())
 		tk.freeze(Date.now())
 		@AuthenticationController = SandboxedModule.require modulePath, requires:
 		@AuthenticationController = SandboxedModule.require modulePath, requires:
 			"./AuthenticationManager": @AuthenticationManager = {}
 			"./AuthenticationManager": @AuthenticationManager = {}
-			"../User/UserUpdater" : @UserUpdater = {}
+			"../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()}
 			"metrics-sharelatex": @Metrics = { inc: sinon.stub() }
 			"metrics-sharelatex": @Metrics = { inc: sinon.stub() }
 			"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
 			"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin:sinon.stub() }
 			"../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()}
 			"../User/UserHandler": @UserHandler = {setupLoginData:sinon.stub()}

+ 0 - 5
services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee

@@ -137,11 +137,6 @@ describe "EditorHttpController", ->
 					.calledWith(@project_id)
 					.calledWith(@project_id)
 					.should.equal true
 					.should.equal true
 
 
-			it "should look up the user", ->
-				@UserGetter.getUser
-					.calledWith(@user_id, { isAdmin: true })
-					.should.equal true
-					
 			it "should check the privilege level", ->
 			it "should check the privilege level", ->
 				@AuthorizationManager.getPrivilegeLevelForProject
 				@AuthorizationManager.getPrivilegeLevelForProject
 					.calledWith(@user_id, @project_id, @token)
 					.calledWith(@user_id, @project_id, @token)

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