فهرست منبع

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 سال پیش
والد
کامیت
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")
 Analytics = require "../Analytics/AnalyticsManager"
 passport = require 'passport'
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 
 module.exports = AuthenticationController =
 
@@ -43,7 +44,7 @@ module.exports = AuthenticationController =
 					return callback(err)
 				req.sessionStore.generate(req)
 				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
 				req.session.user = req.session.passport.user
 				req.session.save (err) ->
@@ -112,6 +113,7 @@ module.exports = AuthenticationController =
 		UserHandler.setupLoginData(user, ()->)
 		LoginRateLimiter.recordSuccessfulLogin(user.email)
 		AuthenticationController._recordSuccessfulLogin(user._id)
+		AuthenticationController.ipMatchCheck(req, user)
 		Analytics.recordEvent(user._id, "user-logged-in", {ip:req.ip})
 		Analytics.identifyUser(user._id, req.sessionID)
 		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
 		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) ->
 		for key, value of props
 			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?
 			CollaboratorsHandler.getInvitedMembersWithPrivilegeLevels project_id, (error, members) ->
 				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?
-					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?
-						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)->
 		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.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({
 	subject: () -> "Activate your #{settings.appName} Account"
 	message: (opts) -> """

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

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

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

@@ -13,13 +13,19 @@ module.exports =
 			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) ->
-			return next(err) if err?
-			logger.log 
+			return err if err?
+			logger.log
 				user_id:user_id
 				project_id: project_id
 				brand_variation_id:brand_variation_id
@@ -30,11 +36,13 @@ module.exports =
 	exportStatus: (req, res) ->
 		{export_id} = req.params
 		ExportsHandler.fetchExport export_id, (err, export_json) ->
-			return next(err) if err?
+			return err if err?
 			parsed_export = JSON.parse(export_json)
 			json = {
 				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
 

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

@@ -20,9 +20,7 @@ module.exports = ExportsHandler = self =
 				callback null, 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 =
 			project: (cb) ->
 				ProjectGetter.getProject project_id, cb
@@ -60,6 +58,11 @@ module.exports = ExportsHandler = self =
 					metadata:
 						compiler: project.compiler
 						imageName: project.imageName
+						title: title
+						description: description
+						author: author
+						license: license
+						show_source: show_source
 				user:
 					id: user_id
 					firstName: user.first_name

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

@@ -1,5 +1,7 @@
 logger = require("logger-sharelatex")
 NotificationsHandler = require("./NotificationsHandler")
+request = require "request"
+settings = require "settings-sharelatex"
 
 module.exports =
 
@@ -29,3 +31,29 @@ module.exports =
 			NotificationsHandler.createNotification user._id, @key, "notification_project_invite", messageOpts, invite.expires, callback
 		read:  (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 = []
 				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 = {
 				key:key
 				messageOpts:messageOpts
 				templateKey:templateKey
-				forceCreate: true
+				forceCreate:forceCreate
 		}
 		if 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'
 Modules = require '../../infrastructure/Modules'
 ProjectEntityHandler = require './ProjectEntityHandler'
+UserGetter = require("../User/UserGetter")
+NotificationsBuilder = require("../Notifications/NotificationsBuilder")
 crypto = require 'crypto'
 { V1ConnectionError } = require '../Errors/Errors'
 Features = require('../../infrastructure/Features')
@@ -209,6 +211,11 @@ module.exports = ProjectController =
 				user = results.user
 				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) ->
 					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"
 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 =
 	index : (req,res)->
@@ -21,11 +22,13 @@ module.exports = HomeController =
 		else
 			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
-			res.redirect "/login"
+			res.redirect '/login'
 
 	externalPage: (page, title) ->
 		return (req, res, next = (error) ->) ->

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

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

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

@@ -3,6 +3,7 @@ AuthenticationController = require '../Authentication/AuthenticationController'
 TokenAccessHandler = require './TokenAccessHandler'
 Errors = require '../Errors/Errors'
 logger = require 'logger-sharelatex'
+settings = require 'settings-sharelatex'
 
 module.exports = TokenAccessController =
 
@@ -11,11 +12,16 @@ module.exports = TokenAccessController =
 		return ProjectController.loadEditor(req, res, next)
 
 	_tryHigherAccess: (token, userId, req, res, next) ->
-		TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project) ->
+		TokenAccessHandler.findProjectWithHigherAccess token, userId, (err, project, projectExists) ->
 			if err?
 				logger.err {err, token, userId},
 					"[TokenAccess] error finding project with higher access"
 				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?
 				logger.log {token, userId},
 					"[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
 		}, {_id: 1, publicAccesLevel: 1, owner_ref: 1}, callback
 
-	findProjectWithHigherAccess: (token, userId, callback=(err, project)->) ->
+	findProjectWithHigherAccess: (token, userId, callback=(err, project, projectExists)->) ->
 		Project.findOne {
 			$or: [
 				{'tokens.readAndWrite': token},
@@ -32,12 +32,16 @@ module.exports = TokenAccessHandler =
 			if err?
 				return callback(err)
 			if !project?
-				return callback(null, null)
+				return callback(null, null, false) # Project doesn't exist, so we handle differently
 			projectId = project._id
 			CollaboratorsHandler.isUserInvitedMemberOfProject userId, projectId, (err, isMember) ->
 				if 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)->) ->
 		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 =
 	getUser: (query, projection, callback = (error, user) ->) ->
+		return callback(new Error("no query provided")) unless query?
 		if query?.email?
 			return callback(new Error("Don't use getUser to find user by email"), null)
 		if arguments.length == 2
@@ -34,7 +35,7 @@ module.exports = UserGetter =
 
 			getUserAffiliations userId, (error, affiliationsData) ->
 				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) ->) ->
 		email = email.trim()

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

@@ -1,20 +1,33 @@
 settings = require("settings-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 metrics.http.monitor(logger)
-app.use RedirectManager
+RedirectManager.apply(webRouter)
 ProxyManager.apply(publicApiRouter)
 
 

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

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

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

@@ -68,6 +68,7 @@ module.exports = class Router
 		webRouter.get  '/logout', UserController.logout
 		webRouter.get  '/restricted', AuthorizationMiddlewear.restricted
 
+
 		if Features.hasFeature('registration')
 			webRouter.get '/register', UserPagesController.registerPage
 			AuthenticationController.addEndpointToLoginWhitelist '/register'
@@ -329,6 +330,21 @@ module.exports = class Router
 			AuthenticationController.httpAuth,
 			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
 		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

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

@@ -18,8 +18,7 @@ mixin linkAdvisors(linkText, linkClass, track)
 		| #{linkText ? linkText : 'advisor programme'}
 
 mixin linkBenefits(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/benefits" class=linkClass ? linkClass : '')
+	a(href="/for/authors" class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'benefits'}
 		
 mixin linkBlog(linkText, linkClass, slug)
@@ -36,8 +35,7 @@ mixin linkDash(linkText, linkClass)
 		| #{linkText ? linkText : 'project dashboard'}
 
 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'}
 
 mixin linkEmail(linkText, linkClass, email)
@@ -66,8 +64,7 @@ mixin linkInvite(linkText, linkClass, track)
 		| #{linkText ? linkText : 'invite your friends'}
 
 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'}
 
 mixin linkPrintNewTab(linkText, linkClass, icon, track)
@@ -121,6 +118,5 @@ mixin linkTweet(linkText, linkClass, tweetText, track)
 	) #{linkText ? linkText : 'tweet'}
 
 mixin linkUniversities(linkText, linkClass)
-	//- To Do: verify path
-	a(href="/universities" class=linkClass ? linkClass : '')
+	a(href="/for/universities" class=linkClass ? linkClass : '')
 		| #{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')
 	head
 		title Something went wrong
-		link(rel="icon", href="/favicon.ico")
+		link(rel="icon", href="/" + settings.brandPrefix + "favicon.ico")
 		if buildCssPath
 			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")

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

@@ -27,8 +27,18 @@ nav.navbar.navbar-default.navbar-main
 
 				// loop over 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
 							li.dropdown(class=item.class, dropdown)
 								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(
 			href
-			ng-class="{ 'btn-full-height-disabled' : !permissions.admin }"
 			ng-click="openShareProjectModal(permissions.admin);"
 			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
 			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")}
 	.modal-body.modal-body-share
 		.container-fluid
-
 			//- 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
 					| #{translate('link_sharing_is_off')}
 					| &nbsp;&nbsp;
@@ -28,7 +27,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						)
 
 			//- 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
 					strong
 						| #{translate('link_sharing_is_on')}.
@@ -57,7 +56,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						pre.access-token(ng-hide="readOnlyTokenLink") #{translate('loading')}...
 
 			//- 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
 					strong(ng-if="project.publicAccesLevel == 'readAndWrite'") #{translate("this_project_is_public")}
 					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
 					span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
 					span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
-				.col-xs-1
+				.col-xs-1(ng-show="isAdmin")
 					a(
 						href
 						tooltip=translate('remove_collaborator')
@@ -102,7 +101,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						ng-click="revokeInvite(invite)"
 					)
 						i.fa.fa-times
-			.row.invite-controls
+			.row.invite-controls(ng-show="isAdmin")
 				form(ng-show="canAddCollaborators")
 					.small #{translate("share_with_your_collabs")}
 					.form-group
@@ -177,8 +176,9 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
 						) #{translate("start_free_trial")}
 
 					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-left
 			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
 
+block vars
+	- var suppressNavContentLinks = true
+
 block content
 	//- We need to do .replace(/\//g, '\\/') do that '</script>' -> '<\/script>'
 	//- 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.
 									div(ng-show="visible")
 										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
 								if userIsFromSL(user)
 									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")
 			if settings.overleaf && settings.overleaf.host
 				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}"
 				) No thanks, open in v1
 			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
 										span(aria-hidden="true") &times;
 										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)
 							div.notification_inner
 								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)
 
 		.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
 			hr

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

@@ -13,7 +13,7 @@
 				ng-show="project.accessLevel == 'owner'"
 			) {{project.name}}
 			a.projectName(
-				href=settings.overleaf.host + "/{{project.id}}"
+				href='/sign_in_to_v1?return_to=/{{project.id}}'
 				target="_blank"
 				ng-hide="project.accessLevel == 'owner'"
 			) {{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
 		h3 #{translate("group_plan_enquiry")}
 	.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")
 				.form-group
 					label#title9(for='Field9')

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

@@ -122,7 +122,7 @@ block content
 							p
 								| You are subscribed to Overleaf through Overleaf v1
 							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
 
 						if settings.overleaf && v1Subscriptions && v1Subscriptions.teams && v1Subscriptions.teams.length > 0
@@ -130,7 +130,7 @@ block content
 								p
 									| You are a member of the Overleaf v1 team: <strong>#{team.name}</strong>
 							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
 
 					.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
 				p
 					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
 						| 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.
@@ -248,7 +248,7 @@ block content
 					div.alert.alert-info
 						| 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
-						| #[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.
 		.modal-footer
 			button.btn.btn-default(

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

@@ -491,7 +491,7 @@ module.exports = settings =
 	modules:
 		sanitize:
 			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:
 					'a': [ 'href', 'name', 'target', 'class' ]
 					'div': [ 'class', 'id', 'style' ]
@@ -504,4 +504,6 @@ module.exports = settings =
 					'figure': [ 'class', 'id', 'style']
 					'figcaption': [ 'class', 'id', 'style']
 					'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": {
       "version": "1.1.3",
       "from": "ci-info@>=1.0.0 <2.0.0",
@@ -3482,6 +3487,12 @@
       "resolved": "https://registry.npmjs.org/flatiron/-/flatiron-0.4.3.tgz",
       "dev": true,
       "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": {
           "version": "0.6.0",
           "from": "optimist@0.6.0",
@@ -3603,6 +3614,11 @@
       "from": "fs-extra@>=4.0.2 <5.0.0",
       "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": {
       "version": "1.0.0",
       "from": "fs.realpath@>=1.0.0 <2.0.0",
@@ -6696,6 +6712,38 @@
       "from": "lynx@0.1.1",
       "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": {
       "version": "3.3.2",
       "from": "mailcomposer@3.3.2",
@@ -6981,9 +7029,31 @@
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
     },
     "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": {
       "version": "1.3.1",
@@ -7002,7 +7072,14 @@
     "mkdirp": {
       "version": "0.5.1",
       "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": {
       "version": "5.0.1",
@@ -7312,6 +7389,12 @@
           "resolved": "https://registry.npmjs.org/async/-/async-0.2.9.tgz",
           "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": {
           "version": "0.6.0",
           "from": "optimist@0.6.0",
@@ -8145,7 +8228,14 @@
     "optimist": {
       "version": "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": {
       "version": "0.8.2",
@@ -8318,6 +8408,11 @@
       "from": "passport@>=0.3.2 <0.4.0",
       "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": {
       "version": "0.6.0",
       "from": "passport-ldapauth@>=0.6.0 <0.7.0",
@@ -8328,6 +8423,11 @@
       "from": "passport-local@>=1.0.0 <2.0.0",
       "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": {
       "version": "1.4.0",
       "from": "passport-oauth2@>=1.4.0 <2.0.0",
@@ -8338,6 +8438,11 @@
       "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"
     },
+    "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": {
       "version": "0.15.0",
       "from": "passport-saml@>=0.15.0 <0.16.0",
@@ -8372,6 +8477,11 @@
       "from": "passport-strategy@>=1.0.0 <2.0.0",
       "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": {
       "version": "0.0.0",
       "from": "path-browserify@0.0.0",
@@ -12502,6 +12612,11 @@
       "from": "xtend@>=4.0.1 <5.0.0",
       "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": {
       "version": "3.2.1",
       "from": "y18n@>=3.2.1 <4.0.0",

+ 5 - 2
services/web/package.json

@@ -63,6 +63,7 @@
     "marked": "^0.3.5",
     "method-override": "^2.3.3",
     "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.7.1",
+    "minimist": "1.2.0",
     "mocha": "^5.0.1",
     "mongojs": "2.4.0",
     "mongoose": "4.11.4",
@@ -75,11 +76,14 @@
     "nvd3": "^1.8.6",
     "optimist": "0.6.1",
     "passport": "^0.3.2",
+    "passport-google-oauth20": "^1.0.0",
     "passport-ldapauth": "^0.6.0",
     "passport-local": "^1.0.0",
     "passport-oauth2": "^1.4.0",
     "passport-oauth2-refresh": "^1.0.0",
+    "passport-orcid": "0.0.3",
     "passport-saml": "^0.15.0",
+    "passport-twitter": "^1.0.4",
     "pug": "^2.0.0-beta6",
     "react": "^15.4.2",
     "react-dom": "^15.4.2",
@@ -99,8 +103,7 @@
     "v8-profiler": "^5.2.3",
     "valid-url": "^1.0.9",
     "xml2js": "0.2.0",
-    "yauzl": "^2.8.0",
-    "minimist": "1.2.0"
+    "yauzl": "^2.8.0"
   },
   "devDependencies": {
     "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', () ->
 					if scope.sharejsDoc?
+						scope.$broadcast('changeEditor')
 						tearDownSpellCheck()
 						tearDownCursorPosition()
 						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) ->
 			@$scope.$on 'editorInit', @jumpToPositionInNewDoc
 
-			@$scope.$on 'beforeChangeDocument', () =>
-				@storeCursorPosition()
-				@storeFirstVisibleLine()
+			@$scope.$on 'beforeChangeDocument', @storePositionAndLine
 
 			@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
 
+			@$scope.$on 'changeEditor', @storePositionAndLine
+
 			@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
 				if line?
 					setTimeout () =>
@@ -24,6 +24,10 @@ define [], () ->
 			@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
 				@adapter.clearSelection()
 
+		storePositionAndLine: () =>
+			@storeCursorPosition()
+			@storeFirstVisibleLine()
+
 		jumpToPositionInNewDoc: () =>
 			@doc_id = @$scope.sharejsDoc?.doc_id
 			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",
 	($scope, $modal, ide, projectInvites, projectMembers, event_tracking) ->
 			$scope.openShareProjectModal = (isAdmin) ->
-				if !isAdmin
-					return
-					
+				$scope.isAdmin = isAdmin;
 				event_tracking.sendMBOnce "ide-open-share-modal-once"
 
 				$modal.open(

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

@@ -24,7 +24,6 @@ define [
 	"main/affiliations/controllers/UserAffiliationsController"
 	"main/affiliations/factories/UserAffiliationsDataService"
 	"main/keys"
-	"main/account-merge-checker"
 	"main/cms/index"
 	"analytics/AbTestingManager"
 	"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"
 	"libs/platform"
 ], (App, platform) ->
-	App.controller 'UniverstiesContactController', ($scope, $modal, $http) ->
+	App.controller 'GroupPlanContactController', ($scope, $modal, $http) ->
 
 		$scope.form = {}
 		$scope.sent = false
@@ -20,7 +20,7 @@ define [
 				email: $scope.form.email
 				labels: "#{$scope.form.source} accounts"
 				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"
 
 			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"
 ], (App) ->
 
-	App.controller 'LeftHandMenuPromoController', ($scope) ->
+	App.controller 'LeftHandMenuPromoController', ($scope, UserAffiliationsDataService) ->
 
 		$scope.hasProjects = window.data.projects.length > 0
 		$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/ol-chat.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 {
       display: inline-block;
       margin: 0;
-      padding: 0 @margin-sm 0 0;
+      padding: 0 @padding-sm @padding-sm 0;
     }
     a {
       font-size: small;
@@ -44,4 +44,16 @@
     border-radius: @border-radius-base;
     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
 */
 .cms-page {
+  img {
+    height: auto;
+    max-width: 100%;
+  }
   .btn-description {
     margin-right: @margin-sm;
   }
@@ -85,4 +89,57 @@
       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 {
 		&:hover {
 			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;
 			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 {
 			top: 4px;
 			right: 4px;

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

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

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

@@ -1,3 +1,5 @@
+@register-v-spacing: 20px;
+
 .deprecated-sl-masthead {
 	display: inline-block;
 	margin-top: @header-height;
@@ -38,15 +40,12 @@
 }
 
 .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;
 	text-align: center;
 	overflow: hidden;
-	margin-top: @header-height;
+	padding-top: @header-height;
 	h1, p, label {
 		color: white;
 		text-align: center;
@@ -70,10 +69,13 @@
 		text-rendering: auto;
 		margin-bottom: @line-height-computed;
 	}
+	label {
+		display: block;
+	}
 	.register-banner {
 		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));
-		padding: @line-height-computed 0;
+		padding: @register-v-spacing 0;
 		position: absolute;
 		bottom: 0;
 		width: 100%;
@@ -83,13 +85,17 @@
 			font-family: @font-family-sans-serif;
 			font-weight: 500;
 			letter-spacing: 1px;
+			margin-bottom: @register-v-spacing;
 		}
 		.form-group {
 			margin-left: @line-height-computed / 2;
 		}
+		.input-lg {
+			border-radius: 9999px;
+		}
 	}
 	.screenshot {
-		height: 550px;
+		height: 600px;
 		margin: auto;
 		margin-bottom: -50px;
 		overflow-y: hidden;
@@ -97,7 +103,7 @@
 		max-width: 960px;
 		.img {
 			max-width: 960px;
-			background-image: url('/img/screen.png');
+			background-image: url('/img/homepage.png');
 			background-size: 100%;
 			background-repeat: no-repeat;
 			margin: auto;
@@ -111,38 +117,75 @@
 		only screen and (                min-resolution: 192dpi),
 		only screen and (                min-resolution: 2dppx) { 
 			.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 {
-	padding-bottom: @line-height-computed;
+	border-bottom: 1px solid @gray-lighter;
 	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;
 }
 
+.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 {
 	.cursor {
 		background-color: hsl(200, 70%, 70%);
@@ -212,3 +261,9 @@
 	}
 	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 {
-		border-radius: 50%;
 		float: right;
-		height: 100px;
-		overflow: hidden;
-		position: relative;
-		width: 100px;
-		img {
-			display: inline;
-			margin: 0 auto;
-			width: 100%;
-		}
 	}
 	.faq {
 		p {

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

@@ -100,51 +100,9 @@
   }
   // End Print
 
-  /*
-    Begin Tabs
-  */
   .nav-tabs {
-    // Overrides for nav.less
     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) {
     .content-pull {

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

@@ -16,100 +16,81 @@
 }
 
 .cta-links {
-	margin-top: @margin-lg;
+	margin-bottom: @margin-md;
 
 	.cta-link.btn {
-		margin: 0 @margin-sm;
+		margin-right: @margin-sm;
 	}
 }
 
 .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__container {
+		display: inline-block;
+		margin: 0 0 1em;
+		width: 100%;
+	}
 
 	.thumbnail {
 		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;
-
-		&.thumbnail-tag {
-			height: 100px;
-		}
-
-		img {
-			width: 100%;
-			height: 100%;
-		}
-	}
-
-	a {
 		padding:0px;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		overflow: hidden;
+
 		h3 {
 			color:@link-color;
 			margin: 10px 0px 10px 20px;
 		}
 
-		display: flex;
-		justify-content: center;
-		align-items: center;
+		&.thumbnail-tag {
+			height: 100px;
+		}
 	}
 
 	.caption {
-        // Override ShareLatex template styles
-        background: none;
-        border: none;
-
+		// Override ShareLatex template styles
+		background: none;
+		border: none;
 		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 (max-width: @screen-md-min) {
 		.thumbnail {
-			height: 400px;
-			width: 300px;
-
 			margin: 5% auto;
 		}
 
-		img {
-			width: 90%;
-		}
-
 		.caption .description {
 			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 {
 		margin-bottom: @margin-xl;
-		margin-top: @margin-xl;
+		margin-top: @margin-md;
 	}
 
 	.template-large-pdf-preview {

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

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

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

@@ -65,6 +65,9 @@
 .alert-danger {
   .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) {
   a {

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

@@ -122,6 +122,10 @@
 // Button Sizes
 // --------------------------------------------------
 
+.btn-xl {
+  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-h2; @line-height-large; @btn-border-radius-large);
+}
+
 .btn-lg {
   // 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);

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

@@ -1,6 +1,6 @@
 // Colors
 .icon-accent {
-  color: @accent-color-secondary;
+  color: @accent-color-secondary!important;
 }
 
 // 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:      darken(@link-color, 15%);
 
-
 //== Typography
 //
 //## Font, line-height, and color for body text, headings, and more.
@@ -82,6 +81,11 @@
 @padding-xs-vertical:       1px;
 @padding-xs-horizontal:     5px;
 
+@padding-sm: 10px;
+@padding-md: 20px;
+@padding-lg: 30px;
+@padding-xl: 40px;
+
 @line-height-large:         1.33;
 @line-height-small:         1.5;
 
@@ -586,6 +590,10 @@
 @alert-danger-text:           @state-danger-text;
 @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
 //

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

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

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

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

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

@@ -30,7 +30,13 @@ describe 'Exports', ->
 			@owner.request {
 				method: 'POST',
 				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) =>
 				throw error if error?
 				expect(response.statusCode).to.equal 200
@@ -42,6 +48,12 @@ describe 'Exports', ->
 			# project details should match
 			expect(project.id).to.equal @project_id
 			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
 			expect(project.historyVersion).to.equal @version
 			# 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) =>
 				expect(body.privilegeLevel).to.equal false
 			, 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) =>
 				return callback(error) if error?
 				@request.post {
-					url: "/login"
+					url: if settings.enableLegacyLogin then "/login/legacy" else "/login"
 					json: { email, @password }
 				}, callback
 

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

@@ -3,6 +3,7 @@ v1Api =
 
 module.exports =
 	enableSubscriptions: true
+	enableLegacyRegistration: true
 
 	features: features =
 		v1_free:
@@ -105,3 +106,25 @@ module.exports =
 			path: (params) -> "/universities/list/#{params.id}"
 		'/institutions/domains': { baseUrl: v1Api.url, path: '/university/domains' }
 		'/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())
 		@AuthenticationController = SandboxedModule.require modulePath, requires:
 			"./AuthenticationManager": @AuthenticationManager = {}
-			"../User/UserUpdater" : @UserUpdater = {}
+			"../User/UserUpdater" : @UserUpdater = {updateUser:sinon.stub()}
 			"metrics-sharelatex": @Metrics = { inc: sinon.stub() }
 			"../Security/LoginRateLimiter": @LoginRateLimiter = { processLoginRequest:sinon.stub(), recordSuccessfulLogin: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)
 					.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", ->
 				@AuthorizationManager.getPrivilegeLevelForProject
 					.calledWith(@user_id, @project_id, @token)

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است