This commit is contained in:
Henry Oswald 2014-10-09 16:45:26 +01:00
commit 9924882b59
31 changed files with 394 additions and 97 deletions

View file

@ -67,3 +67,5 @@ Gemfile.lock
.DS_Store .DS_Store
app/views/external app/views/external
modules

View file

@ -10,7 +10,7 @@ module.exports = (grunt) ->
grunt.loadNpmTasks 'grunt-execute' grunt.loadNpmTasks 'grunt-execute'
grunt.loadNpmTasks 'grunt-bunyan' grunt.loadNpmTasks 'grunt-bunyan'
grunt.initConfig config =
execute: execute:
app: app:
src: "app.js" src: "app.js"
@ -28,10 +28,6 @@ module.exports = (grunt) ->
src: 'app.coffee' src: 'app.coffee'
dest: 'app.js' dest: 'app.js'
BackgroundJobsWorker:
src: 'BackgroundJobsWorker.coffee'
dest: 'BackgroundJobsWorker.js'
sharejs: sharejs:
options: options:
join: true join: true
@ -163,6 +159,75 @@ module.exports = (grunt) ->
"help" "help"
] ]
moduleCompileServerTasks = []
moduleCompileUnitTestTasks = []
moduleUnitTestTasks = []
moduleCompileClientTasks = []
moduleIdeClientSideIncludes = []
moduleMainClientSideIncludes = []
if fs.existsSync "./modules"
for module in fs.readdirSync "./modules"
if fs.existsSync "./modules/#{module}/index.coffee"
config.coffee["module_#{module}_server"] = {
expand: true,
flatten: false,
cwd: "modules/#{module}/app/coffee",
src: ['**/*.coffee'],
dest: "modules/#{module}/app/js",
ext: '.js'
}
config.coffee["module_#{module}_index"] = {
src: "modules/#{module}/index.coffee",
dest: "modules/#{module}/index.js"
}
moduleCompileServerTasks.push "coffee:module_#{module}_server"
moduleCompileServerTasks.push "coffee:module_#{module}_index"
config.coffee["module_#{module}_unit_tests"] = {
expand: true,
flatten: false,
cwd: "modules/#{module}/test/unit/coffee",
src: ['**/*.coffee'],
dest: "modules/#{module}/test/unit/js",
ext: '.js'
}
config.mochaTest["module_#{module}_unit"] = {
src: ["modules/#{module}/test/unit/js/*.js"]
options:
reporter: grunt.option('reporter') or 'spec'
grep: grunt.option("grep")
}
moduleCompileUnitTestTasks.push "coffee:module_#{module}_unit_tests"
moduleUnitTestTasks.push "mochaTest:module_#{module}_unit"
if fs.existsSync "./modules/#{module}/public/coffee/ide/index.coffee"
config.coffee["module_#{module}_client_ide"] = {
expand: true,
flatten: false,
cwd: "modules/#{module}/public/coffee/ide",
src: ['**/*.coffee'],
dest: "public/js/ide/#{module}",
ext: '.js'
}
moduleCompileClientTasks.push "coffee:module_#{module}_client_ide"
moduleIdeClientSideIncludes.push "ide/#{module}/index"
if fs.existsSync "./modules/#{module}/public/coffee/main/index.coffee"
config.coffee["module_#{module}_client_main"] = {
expand: true,
flatten: false,
cwd: "modules/#{module}/public/coffee/main",
src: ['**/*.coffee'],
dest: "public/js/main/#{module}",
ext: '.js'
}
moduleCompileClientTasks.push "coffee:module_#{module}_client_main"
moduleMainClientSideIncludes.push "main/#{module}/index"
grunt.initConfig config
grunt.registerTask 'wrap_sharejs', 'Wrap the compiled ShareJS code for AMD module loading', () -> grunt.registerTask 'wrap_sharejs', 'Wrap the compiled ShareJS code for AMD module loading', () ->
content = fs.readFileSync "public/js/libs/sharejs.js" content = fs.readFileSync "public/js/libs/sharejs.js"
fs.writeFileSync "public/js/libs/sharejs.js", """ fs.writeFileSync "public/js/libs/sharejs.js", """
@ -174,8 +239,20 @@ module.exports = (grunt) ->
grunt.registerTask 'help', 'Display this help list', 'availabletasks' grunt.registerTask 'help', 'Display this help list', 'availabletasks'
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir'] grunt.registerTask 'compile:modules:server', 'Compile all the modules', moduleCompileServerTasks
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs'] grunt.registerTask 'compile:modules:unit_tests', 'Compile all the modules unit tests', moduleCompileUnitTestTasks
grunt.registerTask 'compile:modules:client', 'Compile all the module client side code', moduleCompileClientTasks
grunt.registerTask 'compile:modules:inject_clientside_includes', () ->
content = fs.readFileSync("public/js/ide.js").toString()
content = content.replace(/, "__IDE_CLIENTSIDE_INCLUDES__"/g, moduleIdeClientSideIncludes.map((i) -> ", \"#{i}\"").join(""))
fs.writeFileSync "public/js/ide.js", content
content = fs.readFileSync("public/js/main.js").toString()
content = content.replace(/, "__MAIN_CLIENTSIDE_INCLUDES__"/g, moduleMainClientSideIncludes.map((i) -> ", \"#{i}\"").join(""))
fs.writeFileSync "public/js/main.js", content
grunt.registerTask 'compile:server', 'Compile the server side coffee script', ['clean:app', 'coffee:app', 'coffee:app_dir', 'compile:modules:server']
grunt.registerTask 'compile:client', 'Compile the client side coffee script', ['coffee:client', 'coffee:sharejs', 'wrap_sharejs', "compile:modules:client", 'compile:modules:inject_clientside_includes']
grunt.registerTask 'compile:css', 'Compile the less files to css', ['less'] grunt.registerTask 'compile:css', 'Compile the less files to css', ['less']
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs'] grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs']
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests'] grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
@ -188,6 +265,8 @@ module.exports = (grunt) ->
grunt.registerTask 'test:unit', 'Run the unit tests (use --grep=<regex> or --feature=<feature> for individual tests)', ['compile:server', 'compile:unit_tests', 'mochaTest:unit'] grunt.registerTask 'test:unit', 'Run the unit tests (use --grep=<regex> or --feature=<feature> for individual tests)', ['compile:server', 'compile:unit_tests', 'mochaTest:unit']
grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke'] grunt.registerTask 'test:smoke', 'Run the smoke tests', ['compile:smoke_tests', 'mochaTest:smoke']
grunt.registerTask 'test:modules:unit', 'Run the unit tests for the modules', ['compile:modules:server', 'compile:modules:unit_tests'].concat(moduleUnitTestTasks)
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'bunyan', 'execute'] grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'bunyan', 'execute']
grunt.registerTask 'default', 'run' grunt.registerTask 'default', 'run'

View file

@ -50,6 +50,10 @@ module.exports = AuthenticationController =
callback null, null callback null, null
getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) -> getLoggedInUser: (req, options = {allow_auth_token: false}, callback = (error, user) ->) ->
if typeof(options) == "function"
callback = options
options = {allow_auth_token: false}
if req.session?.user?._id? if req.session?.user?._id?
query = req.session.user._id query = req.session.user._id
else if req.query?.auth_token? and options.allow_auth_token else if req.query?.auth_token? and options.allow_auth_token

View file

@ -59,6 +59,10 @@ module.exports = EditorController =
# can be done affter the connection has happened # can be done affter the connection has happened
ConnectedUsersManager.updateUserPosition project_id, client.id, user, null, -> ConnectedUsersManager.updateUserPosition project_id, client.id, user, null, ->
# Only show the 'renamed or deleted' message once
ProjectDeleter.unmarkAsDeletedByExternalSource project
leaveProject: (client, user) -> leaveProject: (client, user) ->
self = @ self = @
client.get "project_id", (error, project_id) -> client.get "project_id", (error, project_id) ->

View file

@ -34,9 +34,10 @@ module.exports =
setNewUserPassword: (req, res)-> setNewUserPassword: (req, res)->
{passwordResetToken, password} = req.body {passwordResetToken, password} = req.body
if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0 if !password? or password.length == 0 or !passwordResetToken? or passwordResetToken.length == 0
return res.send 500 return res.send 400
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err)-> PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found) ->
if err? return next(err) if err?
res.send 500 if found
else
res.send 200 res.send 200
else
res.send 404, {message: req.i18n.translate("password_reset_token_expired")}

View file

@ -23,10 +23,11 @@ module.exports =
return callback(error) if error? return callback(error) if error?
callback null, true callback null, true
setNewUserPassword: (token, password, callback)-> setNewUserPassword: (token, password, callback = (error, found) ->)->
PasswordResetTokenHandler.getUserIdFromTokenAndExpire token, (err, user_id)-> PasswordResetTokenHandler.getUserIdFromTokenAndExpire token, (err, user_id)->
if err then return callback(err) if err then return callback(err)
if !user_id? if !user_id?
logger.err user_id:user_id, "token for password reset did not find user_id" return callback null, false
return callback("no user found") AuthenticationManager.setUserPassword user_id, password, (err) ->
AuthenticationManager.setUserPassword user_id, password, callback if err then return callback(err)
callback null, true

View file

@ -128,6 +128,8 @@ module.exports = ProjectController =
Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb Project.findAllUsersProjects user_id, 'name lastUpdated publicAccesLevel archived owner_ref', cb
hasSubscription: (cb)-> hasSubscription: (cb)->
LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb LimitationsManager.userHasSubscriptionOrIsGroupMember req.session.user, cb
user: (cb) ->
User.findById user_id, "featureSwitches", cb
}, (err, results)-> }, (err, results)->
if err? if err?
logger.err err:err, "error getting data for project list page" logger.err err:err, "error getting data for project list page"
@ -135,6 +137,7 @@ module.exports = ProjectController =
logger.log results:results, user_id:user_id, "rendering project list" logger.log results:results, user_id:user_id, "rendering project list"
tags = results.tags[0] tags = results.tags[0]
projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2] projects = ProjectController._buildProjectList results.projects[0], results.projects[1], results.projects[2]
user = results.user
ProjectController._injectProjectOwners projects, (error, projects) -> ProjectController._injectProjectOwners projects, (error, projects) ->
return next(error) if error? return next(error) if error?
@ -143,6 +146,7 @@ module.exports = ProjectController =
priority_title: true priority_title: true
projects: projects projects: projects
tags: tags tags: tags
user: user
hasSubscription: results.hasSubscription hasSubscription: results.hasSubscription
} }
@ -212,6 +216,7 @@ module.exports = ProjectController =
referal_id : user.referal_id referal_id : user.referal_id
subscription : subscription :
freeTrial: {allowed: allowedFreeTrial} freeTrial: {allowed: allowedFreeTrial}
featureSwitches: user.featureSwitches
} }
userSettings: { userSettings: {
mode : user.ace.mode mode : user.ace.mode
@ -283,8 +288,7 @@ defaultSettingsForAnonymousUser = (user_id)->
freeTrial: freeTrial:
allowed: true allowed: true
featureSwitches: featureSwitches:
dropbox: false github: false
trackChanges: false
THEME_LIST = [] THEME_LIST = []
do generateThemeList = () -> do generateThemeList = () ->

View file

@ -7,7 +7,7 @@ FileStoreHandler = require("../FileStore/FileStoreHandler")
module.exports = ProjectDeleter = module.exports = ProjectDeleter =
markAsDeletedByExternalSource : (project_id, callback)-> markAsDeletedByExternalSource : (project_id, callback = (error) ->)->
logger.log project_id:project_id, "marking project as deleted by external data source" logger.log project_id:project_id, "marking project as deleted by external data source"
conditions = {_id:project_id} conditions = {_id:project_id}
update = {deletedByExternalDataSource:true} update = {deletedByExternalDataSource:true}
@ -16,6 +16,15 @@ module.exports = ProjectDeleter =
require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed project_id, -> require('../Editor/EditorController').notifyUsersProjectHasBeenDeletedOrRenamed project_id, ->
callback() callback()
unmarkAsDeletedByExternalSource: (project, callback = (error) ->) ->
logger.log project_id: "removing flag marking project as deleted by external data source"
if project.deletedByExternalDataSource
conditions = {_id:project._id.toString()}
update = {deletedByExternalDataSource: false}
Project.update conditions, update, {}, callback
else
callback()
deleteUsersProjects: (owner_id, callback)-> deleteUsersProjects: (owner_id, callback)->
logger.log owner_id:owner_id, "deleting users projects" logger.log owner_id:owner_id, "deleting users projects"
Project.remove owner_ref:owner_id, callback Project.remove owner_ref:owner_id, callback

View file

@ -1,4 +1,5 @@
tpdsUpdateHandler = require('./TpdsUpdateHandler') tpdsUpdateHandler = require('./TpdsUpdateHandler')
UpdateMerger = require "./UpdateMerger"
logger = require('logger-sharelatex') logger = require('logger-sharelatex')
Path = require('path') Path = require('path')
metrics = require("../../infrastructure/Metrics") metrics = require("../../infrastructure/Metrics")
@ -32,6 +33,24 @@ module.exports =
res.send 200 res.send 200
req.session.destroy() req.session.destroy()
updateProjectContents: (req, res, next = (error) ->) ->
{project_id} = req.params
path = "/" + req.params[0] # UpdateMerger expects leading slash
logger.log project_id: project_id, path: path, "received project contents update"
UpdateMerger.mergeUpdate project_id, path, req, (error) ->
return next(error) if error?
res.send(200)
req.session.destroy()
deleteProjectContents: (req, res, next = (error) ->) ->
{project_id} = req.params
path = "/" + req.params[0] # UpdateMerger expects leading slash
logger.log project_id: project_id, path: path, "received project contents delete request"
UpdateMerger.deleteUpdate project_id, path, (error) ->
return next(error) if error?
res.send(200)
req.session.destroy()
parseParams: parseParams = (req)-> parseParams: parseParams = (req)->
path = req.params[0] path = req.params[0]
user_id = req.params.user_id user_id = req.params.user_id

View file

@ -33,8 +33,7 @@ module.exports =
dropboxHandler.getUserRegistrationStatus user._id, (err, status)-> dropboxHandler.getUserRegistrationStatus user._id, (err, status)->
userIsRegisteredWithDropbox = !err? and status.registered userIsRegisteredWithDropbox = !err? and status.registered
res.render 'user/settings', res.render 'user/settings',
title:'account_settings', title:'account_settings'
userHasDropboxFeature: user.features.dropbox
userIsRegisteredWithDropbox: userIsRegisteredWithDropbox userIsRegisteredWithDropbox: userIsRegisteredWithDropbox
user: user, user: user,
languages: Settings.languages, languages: Settings.languages,

View file

@ -6,6 +6,7 @@ SubscriptionFormatters = require('../Features/Subscription/SubscriptionFormatter
querystring = require('querystring') querystring = require('querystring')
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager") SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
_ = require("underscore") _ = require("underscore")
Modules = require "./Modules"
fingerprints = {} fingerprints = {}
Path = require 'path' Path = require 'path'
@ -146,3 +147,9 @@ module.exports = (app)->
res.locals.currentLngCode = req.lng res.locals.currentLngCode = req.lng
next() next()
app.use (req, res, next) ->
if Settings.reloadModuleViewsOnEachRequest
Modules.loadViewIncludes()
res.locals.moduleIncludes = Modules.moduleIncludes
next()

View file

@ -0,0 +1,36 @@
fs = require "fs"
Path = require "path"
jade = require "jade"
MODULE_BASE_PATH = Path.resolve(__dirname + "/../../../modules")
module.exports = Modules =
modules: []
loadModules: () ->
for moduleName in fs.readdirSync(MODULE_BASE_PATH)
if fs.existsSync(Path.join(MODULE_BASE_PATH, moduleName, "index.js"))
loadedModule = require(Path.join(MODULE_BASE_PATH, moduleName, "index"))
loadedModule.name = moduleName
@modules.push loadedModule
applyRouter: (app) ->
for module in @modules
module.router?.apply(app)
viewIncludes: {}
loadViewIncludes: (app) ->
@viewIncludes = {}
for module in @modules
for view, partial of module.viewIncludes or {}
@viewIncludes[view] ||= []
@viewIncludes[view].push fs.readFileSync(Path.join(MODULE_BASE_PATH, module.name, "app/views", partial + ".jade"))
moduleIncludes: (view, locals) ->
partials = Modules.viewIncludes[view] or []
html = ""
for partial in partials
compiler = jade.compile(partial, doctype: "html")
html += compiler(locals)
return html
Modules.loadModules()

View file

@ -24,6 +24,7 @@ ReferalConnect = require('../Features/Referal/ReferalConnect')
RedirectManager = require("./RedirectManager") RedirectManager = require("./RedirectManager")
OldAssetProxy = require("./OldAssetProxy") OldAssetProxy = require("./OldAssetProxy")
translations = require("translations-sharelatex").setup(Settings.i18n) translations = require("translations-sharelatex").setup(Settings.i18n)
Modules = require "./Modules"
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongojs/node_modules/mongodb"), logger)
metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger) metrics.mongodb.monitor(Path.resolve(__dirname + "/../../../node_modules/mongoose/node_modules/mongodb"), logger)
@ -46,13 +47,13 @@ app.ignoreCsrf = (method, route) ->
ignoreCsrfRoutes.push new express.Route(method, route) ignoreCsrfRoutes.push new express.Route(method, route)
app.configure () -> app.configure () ->
if Settings.behindProxy if Settings.behindProxy
app.enable('trust proxy') app.enable('trust proxy')
app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge }) app.use express.static(__dirname + '/../../../public', {maxAge: staticCacheAge })
app.set 'views', __dirname + '/../../views' app.set 'views', __dirname + '/../../views'
app.set 'view engine', 'jade' app.set 'view engine', 'jade'
Modules.loadViewIncludes app
app.use express.bodyParser(uploadDir: Settings.path.uploadFolder) app.use express.bodyParser(uploadDir: Settings.path.uploadFolder)
app.use express.bodyParser(uploadDir: __dirname + "/../../../data/uploads") app.use express.bodyParser(uploadDir: __dirname + "/../../../data/uploads")
app.use translations.expressMiddlewear app.use translations.expressMiddlewear

View file

@ -33,8 +33,7 @@ UserSchema = new Schema
dropbox: { type:Boolean, default: Settings.defaultFeatures.dropbox } dropbox: { type:Boolean, default: Settings.defaultFeatures.dropbox }
} }
featureSwitches : { featureSwitches : {
dropbox: {type:Boolean, default:true}, github: {type: Boolean}
oldHistory: {type:Boolean}
} }
referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} referal_id : {type:String, default:() -> uuid.v4().split("-")[0]}
refered_users: [ type:ObjectId, ref:'User' ] refered_users: [ type:ObjectId, ref:'User' ]

View file

@ -39,6 +39,7 @@ WikiController = require("./Features/Wiki/WikiController")
ConnectedUsersController = require("./Features/ConnectedUsers/ConnectedUsersController") ConnectedUsersController = require("./Features/ConnectedUsers/ConnectedUsersController")
DropboxRouter = require "./Features/Dropbox/DropboxRouter" DropboxRouter = require "./Features/Dropbox/DropboxRouter"
dropboxHandler = require "./Features/Dropbox/DropboxHandler" dropboxHandler = require "./Features/Dropbox/DropboxHandler"
Modules = require "./infrastructure/Modules"
logger = require("logger-sharelatex") logger = require("logger-sharelatex")
_ = require("underscore") _ = require("underscore")
@ -68,6 +69,8 @@ module.exports = class Router
TemplatesRouter.apply(app) TemplatesRouter.apply(app)
DropboxRouter.apply(app) DropboxRouter.apply(app)
Modules.applyRouter(app)
app.get '/blog', BlogController.getIndexPage app.get '/blog', BlogController.getIndexPage
app.get '/blog/*', BlogController.getPage app.get '/blog/*', BlogController.getPage
@ -155,6 +158,11 @@ module.exports = class Router
app.ignoreCsrf('post', '/user/:user_id/update/*') app.ignoreCsrf('post', '/user/:user_id/update/*')
app.ignoreCsrf('delete', '/user/:user_id/update/*') app.ignoreCsrf('delete', '/user/:user_id/update/*')
app.post '/project/:project_id/contents/*', httpAuth, TpdsController.updateProjectContents
app.del '/project/:project_id/contents/*', httpAuth, TpdsController.deleteProjectContents
app.ignoreCsrf('post', '/project/:project_id/contents/*')
app.ignoreCsrf('delete', '/project/:project_id/contents/*')
app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi app.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi
app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi app.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi

View file

@ -46,15 +46,17 @@ aside#left-menu.full-size(
i.fa.fa-external-link.fa-fw i.fa.fa-external-link.fa-fw
| &nbsp;&nbsp; #{translate("publish_as_template")} | &nbsp;&nbsp; #{translate("publish_as_template")}
div(ng-show="permissions.admin")
span(ng-controller="DropboxController", ng-show="permissions.admin")
h4() #{translate("sync")} h4() #{translate("sync")}
span(ng-controller="DropboxController")
ul.list-unstyled.nav() ul.list-unstyled.nav()
li li
a(ng-click="openDropboxModal()") a(ng-click="openDropboxModal()")
i.fa.fa-dropbox.fa-fw i.fa.fa-dropbox.fa-fw
| &nbsp;&nbsp; Dropbox | &nbsp;&nbsp; Dropbox
!{moduleIncludes("editorLeftMenu", locals)}
h4(ng-show="!anonymous") #{translate("settings")} h4(ng-show="!anonymous") #{translate("settings")}
form.settings(ng-controller="SettingsController", ng-show="!anonymous") form.settings(ng-controller="SettingsController", ng-show="!anonymous")
.containter-fluid .containter-fluid

View file

@ -21,6 +21,7 @@
href, href,
ng-click="openUploadProjectModal()" ng-click="openUploadProjectModal()"
) #{translate("upload_project")} ) #{translate("upload_project")}
!{moduleIncludes("newProjectMenu", locals)}
if (templates) if (templates)
li.divider li.divider
li.dropdown-header #{translate("templates")} li.dropdown-header #{translate("templates")}

View file

@ -96,22 +96,30 @@ block content
ng-disabled="changePasswordForm.$invalid" ng-disabled="changePasswordForm.$invalid"
) #{translate("change")} ) #{translate("change")}
hr.soften hr
h3 #{translate("dropbox_integration")} h3 #{translate("dropbox_integration")}
span.small span.small
a(href='/help/kb/dropbox-2') (#{translate("learn_more")}) a(href='/help/kb/dropbox-2') (#{translate("learn_more")})
- if(!userHasDropboxFeature) - if(!user.features.dropbox)
.alert.alert-info #{translate("dropbox_is_premium")} &nbsp; &nbsp; p.small #{translate("dropbox_sync_description")}
.alert.alert-info
p #{translate("dropbox_is_premium")}
p
a.btn.btn-info(href='/user/subscription/plans') #{translate("upgrade")} a.btn.btn-info(href='/user/subscription/plans') #{translate("upgrade")}
- else if(userIsRegisteredWithDropbox) - else if(userIsRegisteredWithDropbox)
.alert.alert-success #{translate("account_is_linked")} .alert.alert-success
row | #{translate("account_is_linked")}.
a(href='/dropbox/unlink').btn #{translate("unlink_dropbox")} |
a(href='/dropbox/unlink') #{translate("unlink_dropbox")}
- else - else
p.small #{translate("dropbox_sync_description")}
p
a.btn.btn-info(href='/dropbox/beginAuth') #{translate("link_to_dropbox")} a.btn.btn-info(href='/dropbox/beginAuth') #{translate("link_to_dropbox")}
hr.soften | !{moduleIncludes("userSettings", locals)}
hr
p.small p.small
| #{translate("newsletter_info_and_unsubscribe")} | #{translate("newsletter_info_and_unsubscribe")}

View file

@ -90,6 +90,8 @@ module.exports =
url: "http://localhost:3013" url: "http://localhost:3013"
templates: templates:
url: "http://localhost:3007" url: "http://localhost:3007"
githubSync:
url: "http://localhost:3022"
recurly: recurly:
privateKey: "" privateKey: ""
apiKey: "" apiKey: ""
@ -305,3 +307,4 @@ module.exports =
proxyUrls: {} proxyUrls: {}
reloadModuleViewsOnEachRequest: true

View file

@ -17,6 +17,7 @@ define [
"ide/hotkeys/index" "ide/hotkeys/index"
"ide/directives/layout" "ide/directives/layout"
"ide/services/ide" "ide/services/ide"
"__IDE_CLIENTSIDE_INCLUDES__"
"analytics/AbTestingManager" "analytics/AbTestingManager"
"directives/focus" "directives/focus"
"directives/fineUpload" "directives/fineUpload"

View file

@ -78,6 +78,34 @@ define [
readOnly: true readOnly: true
editor.commands.removeCommand "replace" editor.commands.removeCommand "replace"
# Bold text on CMD+B
editor.commands.addCommand
name: "bold",
bindKey: win: "Ctrl-B", mac: "Command-B"
exec: (editor) ->
selection = editor.getSelection()
if selection.isEmpty()
editor.insert("\\textbf{}")
editor.navigateLeft(1)
else
text = editor.getCopyText()
editor.insert("\\textbf{" + text + "}")
readOnly: false
# Italicise text on CMD+I
editor.commands.addCommand
name: "italics",
bindKey: win: "Ctrl-I", mac: "Command-I"
exec: (editor) ->
selection = editor.getSelection()
if selection.isEmpty()
editor.insert("\\textit{}")
editor.navigateLeft(1)
else
text = editor.getCopyText()
editor.insert("\\textit{" + text + "}")
readOnly: false
scope.$watch "onCtrlEnter", (callback) -> scope.$watch "onCtrlEnter", (callback) ->
if callback? if callback?
editor.commands.addCommand editor.commands.addCommand

View file

@ -22,5 +22,6 @@ define [
"directives/selectAll" "directives/selectAll"
"directives/maxHeight" "directives/maxHeight"
"filters/formatDate" "filters/formatDate"
"__MAIN_CLIENTSIDE_INCLUDES__"
], () -> ], () ->
angular.bootstrap(document.body, ["SharelatexApp"]) angular.bootstrap(document.body, ["SharelatexApp"])

View file

@ -196,11 +196,12 @@ define [
$scope.createTag = (name) -> $scope.createTag = (name) ->
event_tracking.send 'project-list-page-interaction', 'project action', 'createTag' event_tracking.send 'project-list-page-interaction', 'project action', 'createTag'
$scope.tags.push { $scope.tags.push tag = {
name: name name: name
project_ids: [] project_ids: []
showWhenEmpty: true showWhenEmpty: true
} }
return tag
$scope.openNewTagModal = (e) -> $scope.openNewTagModal = (e) ->
modalInstance = $modal.open( modalInstance = $modal.open(
@ -210,11 +211,8 @@ define [
modalInstance.result.then( modalInstance.result.then(
(newTagName) -> (newTagName) ->
$scope.createTag(newTagName) tag = $scope.createTag(newTagName)
console.log $scope.tag, $scope.addSelectedProjectsToTag, newTagName $scope.addSelectedProjectsToTag(tag)
$scope.addSelectedProjectsToTag($scope.tag)
# add selected projects to this new tag
) )
$scope.createProject = (name, template = "none") -> $scope.createProject = (name, template = "none") ->

View file

@ -159,15 +159,16 @@
position: absolute; position: absolute;
z-index: 2; z-index: 2;
} }
.highlights-before-label { .highlights-before-label, .highlights-after-label {
position: absolute; position: absolute;
top: @line-height-computed / 2;
right: @line-height-computed; right: @line-height-computed;
z-index: 1;
}
.highlights-before-label {
top: @line-height-computed / 2;
} }
.highlights-after-label { .highlights-after-label {
position: absolute;
bottom: @line-height-computed / 2; bottom: @line-height-computed / 2;
right: @line-height-computed;
} }
} }

View file

@ -22,17 +22,14 @@
} }
.recurly .plan { .recurly .plan {
color: #333; color: #333;
overflow: hidden;
position: relative;
zoom: 1;
font-family: @font-family-serif; font-family: @font-family-serif;
padding: 0 20px 40px;
position: relative;
margin-top: -20px;
} }
.recurly .plan .name { .recurly .plan .name {
float: left;
font-size: 32px; font-size: 32px;
min-width: 200px; margin-right: 130px;
padding-left: 20px;
padding-right: 40px;
} }
.recurly .plan .quantity.field { .recurly .plan .quantity.field {
clear: none; clear: none;
@ -56,9 +53,10 @@
color: #666; color: #666;
} }
.recurly .plan .recurring_cost { .recurly .plan .recurring_cost {
float: right;
text-align: right; text-align: right;
padding-right: 20px; position: absolute;
right: 20px;
top: 0;
} }
.recurly .plan .recurring_cost .cost { .recurly .plan .recurring_cost .cost {
font-size: 32px; font-size: 32px;
@ -68,14 +66,7 @@
padding-bottom: 20px; padding-bottom: 20px;
} }
.recurly .free_trial { .recurly .free_trial {
clear: left;
float: left;
font-size: 13px; font-size: 13px;
height: 22px;
margin: 0;
position: absolute;
top: 35px;
left: 20px;
font-style: italic; font-style: italic;
} }
.recurly .setup_fee { .recurly .setup_fee {

View file

@ -89,6 +89,7 @@ describe "EditorController", ->
@AuthorizationManager.setPrivilegeLevelOnClient = sinon.stub() @AuthorizationManager.setPrivilegeLevelOnClient = sinon.stub()
@EditorRealTimeController.emitToRoom = sinon.stub() @EditorRealTimeController.emitToRoom = sinon.stub()
@ConnectedUsersManager.updateUserPosition.callsArgWith(4) @ConnectedUsersManager.updateUserPosition.callsArgWith(4)
@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
describe "when authorized", -> describe "when authorized", ->
beforeEach -> beforeEach ->
@ -123,6 +124,10 @@ describe "EditorController", ->
it "should mark the user as connected with the ConnectedUsersManager", -> it "should mark the user as connected with the ConnectedUsersManager", ->
@ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, @user, null).should.equal true @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.id, @user, null).should.equal true
it "should remove the flag to send a user a message about the project being deleted", ->
@ProjectDeleter.unmarkAsDeletedByExternalSource
.calledWith(@project)
.should.equal true
describe "when not authorized", -> describe "when not authorized", ->
beforeEach -> beforeEach ->

View file

@ -87,34 +87,34 @@ describe "PasswordResetController", ->
describe "setNewUserPassword", -> describe "setNewUserPassword", ->
it "should tell the user handler to reset the password", (done)-> it "should tell the user handler to reset the password", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2) @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, true)
@res.send = (code)=> @res.send = (code)=>
code.should.equal 200 code.should.equal 200
@PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true @PasswordResetHandler.setNewUserPassword.calledWith(@token, @password).should.equal true
done() done()
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it "should send a 500 if there is an error", (done)-> it "should send 404 if the token didn't work", (done)->
@PasswordResetHandler.setNewUserPassword.callsArgWith(2, "error") @PasswordResetHandler.setNewUserPassword.callsArgWith(2, null, false)
@res.send = (code)=> @res.send = (code)=>
code.should.equal 500 code.should.equal 404
done() done()
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it "should error if there is no password", (done)-> it "should return 400 (Bad Request) if there is no password", (done)->
@req.body.password = "" @req.body.password = ""
@PasswordResetHandler.setNewUserPassword.callsArgWith(2) @PasswordResetHandler.setNewUserPassword.callsArgWith(2)
@res.send = (code)=> @res.send = (code)=>
code.should.equal 500 code.should.equal 400
@PasswordResetHandler.setNewUserPassword.called.should.equal false @PasswordResetHandler.setNewUserPassword.called.should.equal false
done() done()
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res
it "should error if there is no password", (done)-> it "should return 400 (Bad Request) if there is no passwordResetToken", (done)->
@req.body.passwordResetToken = "" @req.body.passwordResetToken = ""
@PasswordResetHandler.setNewUserPassword.callsArgWith(2) @PasswordResetHandler.setNewUserPassword.callsArgWith(2)
@res.send = (code)=> @res.send = (code)=>
code.should.equal 500 code.should.equal 400
@PasswordResetHandler.setNewUserPassword.called.should.equal false @PasswordResetHandler.setNewUserPassword.called.should.equal false
done() done()
@PasswordResetController.setNewUserPassword @req, @res @PasswordResetController.setNewUserPassword @req, @res

View file

@ -63,17 +63,18 @@ describe "PasswordResetHandler", ->
describe "setNewUserPassword", -> describe "setNewUserPassword", ->
it "should return err if no user id can be found", (done)-> it "should return false if no user id can be found", (done)->
@PasswordResetTokenHandler.getUserIdFromTokenAndExpire.callsArgWith(1) @PasswordResetTokenHandler.getUserIdFromTokenAndExpire.callsArgWith(1)
@PasswordResetHandler.setNewUserPassword @token, @password, (err)=> @PasswordResetHandler.setNewUserPassword @token, @password, (err, found) =>
err.should.exists found.should.equal false
@AuthenticationManager.setUserPassword.called.should.equal false @AuthenticationManager.setUserPassword.called.should.equal false
done() done()
it "should set the user password", (done)-> it "should set the user password", (done)->
@PasswordResetTokenHandler.getUserIdFromTokenAndExpire.callsArgWith(1, null, @user_id) @PasswordResetTokenHandler.getUserIdFromTokenAndExpire.callsArgWith(1, null, @user_id)
@AuthenticationManager.setUserPassword.callsArgWith(2) @AuthenticationManager.setUserPassword.callsArgWith(2)
@PasswordResetHandler.setNewUserPassword @token, @password, (err)=> @PasswordResetHandler.setNewUserPassword @token, @password, (err, found) =>
found.should.equal true
@AuthenticationManager.setUserPassword.calledWith(@user_id, @password).should.equal true @AuthenticationManager.setUserPassword.calledWith(@user_id, @password).should.equal true
done() done()

View file

@ -198,8 +198,8 @@ describe "ProjectController", ->
first_name: 'James' first_name: 'James'
'user-2': 'user-2':
first_name: 'Henry' first_name: 'Henry'
@users[@user._id] = @user # Owner
@UserModel.findById = (id, fields, callback) => @UserModel.findById = (id, fields, callback) =>
fields.should.equal 'first_name last_name'
callback null, @users[id] callback null, @users[id]
@LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false) @LimitationsManager.userHasSubscriptionOrIsGroupMember.callsArgWith(1, null, false)
@ -224,6 +224,12 @@ describe "ProjectController", ->
done() done()
@ProjectController.projectListPage @req, @res @ProjectController.projectListPage @req, @res
it "should send the user", (done)->
@res.render = (pageName, opts)=>
opts.user.should.deep.equal @user
done()
@ProjectController.projectListPage @req, @res
it "should inject the users", (done) -> it "should inject the users", (done) ->
@res.render = (pageName, opts)=> @res.render = (pageName, opts)=>
opts.projects[0].owner.should.equal (@users[@projects[0].owner_ref]) opts.projects[0].owner.should.equal (@users[@projects[0].owner_ref])
@ -275,6 +281,7 @@ describe "ProjectController", ->
@UserModel.findById.callsArgWith(1, null, @user) @UserModel.findById.callsArgWith(1, null, @user)
@SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {}) @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, {})
@SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner" @SecurityManager.userCanAccessProject.callsArgWith 2, true, "owner"
@ProjectDeleter.unmarkAsDeletedByExternalSource = sinon.stub()
it "should render the project/editor page", (done)-> it "should render the project/editor page", (done)->
@res.render = (pageName, opts)=> @res.render = (pageName, opts)=>

View file

@ -30,6 +30,7 @@ describe 'Project deleter', ->
'../../models/Project':{Project:@Project} '../../models/Project':{Project:@Project}
'../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler '../DocumentUpdater/DocumentUpdaterHandler': @documentUpdaterHandler
"../Tags/TagsHandler":@TagsHandler "../Tags/TagsHandler":@TagsHandler
"../FileStore/FileStoreHandler": @FileStoreHandler = {}
'logger-sharelatex': 'logger-sharelatex':
log:-> log:->
@ -47,6 +48,32 @@ describe 'Project deleter', ->
@editorController.notifyUsersProjectHasBeenDeletedOrRenamed.calledWith(project_id).should.equal true @editorController.notifyUsersProjectHasBeenDeletedOrRenamed.calledWith(project_id).should.equal true
done() done()
describe "unmarkAsDeletedByExternalSource", ->
beforeEach ->
@Project.update = sinon.stub().callsArg(3)
@callback = sinon.stub()
@project = {
_id: @project_id
}
describe "when the project does not have the flag set", ->
beforeEach ->
@project.deletedByExternalDataSource = false
@deleter.unmarkAsDeletedByExternalSource @project, @callback
it "should not update the project", ->
@Project.update.called.should.equal false
describe "when the project does have the flag set", ->
beforeEach ->
@project.deletedByExternalDataSource = true
@deleter.unmarkAsDeletedByExternalSource @project, @callback
it "should remove the flag from the project", ->
@Project.update
.calledWith({_id: @project_id}, {deletedByExternalDataSource:false})
.should.equal true
describe "deleteUsersProjects", -> describe "deleteUsersProjects", ->
it "should remove all the projects owned by the user_id", (done)-> it "should remove all the projects owned by the user_id", (done)->

View file

@ -5,11 +5,12 @@ modulePath = require('path').join __dirname, '../../../../app/js/Features/ThirdP
describe 'third party data store', -> describe 'TpdsController', ->
beforeEach -> beforeEach ->
@updateHandler = {} @TpdsUpdateHandler = {}
@controller = SandboxedModule.require modulePath, requires: @TpdsController = SandboxedModule.require modulePath, requires:
'./TpdsUpdateHandler':@updateHandler './TpdsUpdateHandler':@TpdsUpdateHandler
'./UpdateMerger': @UpdateMerger = {}
'logger-sharelatex': 'logger-sharelatex':
log:-> log:->
err:-> err:->
@ -24,11 +25,11 @@ describe 'third party data store', ->
params:{0:path, "user_id":@user_id} params:{0:path, "user_id":@user_id}
session: session:
destroy:-> destroy:->
@updateHandler.newUpdate = sinon.stub().callsArg(5) @TpdsUpdateHandler.newUpdate = sinon.stub().callsArg(5)
res = send: => res = send: =>
@updateHandler.newUpdate.calledWith(@user_id, "projectName","/here.txt", req).should.equal true @TpdsUpdateHandler.newUpdate.calledWith(@user_id, "projectName","/here.txt", req).should.equal true
done() done()
@controller.mergeUpdate req, res @TpdsController.mergeUpdate req, res
describe 'getting a delete update', -> describe 'getting a delete update', ->
it 'should process the delete with the update reciver', (done)-> it 'should process the delete with the update reciver', (done)->
@ -37,18 +38,18 @@ describe 'third party data store', ->
params:{0:path, "user_id":@user_id} params:{0:path, "user_id":@user_id}
session: session:
destroy:-> destroy:->
@updateHandler.deleteUpdate = sinon.stub().callsArg(4) @TpdsUpdateHandler.deleteUpdate = sinon.stub().callsArg(4)
res = send: => res = send: =>
@updateHandler.deleteUpdate.calledWith(@user_id, "projectName", "/here.txt").should.equal true @TpdsUpdateHandler.deleteUpdate.calledWith(@user_id, "projectName", "/here.txt").should.equal true
done() done()
@controller.deleteUpdate req, res @TpdsController.deleteUpdate req, res
describe 'parseParams', -> describe 'parseParams', ->
it 'should take the project name off the start and replace with slash', -> it 'should take the project name off the start and replace with slash', ->
path = "noSlashHere" path = "noSlashHere"
req = params:{0:path, user_id:@user_id} req = params:{0:path, user_id:@user_id}
result = @controller.parseParams(req) result = @TpdsController.parseParams(req)
result.user_id.should.equal @user_id result.user_id.should.equal @user_id
result.filePath.should.equal "/" result.filePath.should.equal "/"
result.projectName.should.equal path result.projectName.should.equal path
@ -57,7 +58,7 @@ describe 'third party data store', ->
it 'should take the project name off the start and return it with no slashes in', -> it 'should take the project name off the start and return it with no slashes in', ->
path = "/project/file.tex" path = "/project/file.tex"
req = params:{0:path, user_id:@user_id} req = params:{0:path, user_id:@user_id}
result = @controller.parseParams(req) result = @TpdsController.parseParams(req)
result.user_id.should.equal @user_id result.user_id.should.equal @user_id
result.filePath.should.equal "/file.tex" result.filePath.should.equal "/file.tex"
result.projectName.should.equal "project" result.projectName.should.equal "project"
@ -65,8 +66,57 @@ describe 'third party data store', ->
it 'should take the project name of and return a slash for the file path', -> it 'should take the project name of and return a slash for the file path', ->
path = "/project_name" path = "/project_name"
req = params:{0:path, user_id:@user_id} req = params:{0:path, user_id:@user_id}
result = @controller.parseParams(req) result = @TpdsController.parseParams(req)
result.projectName.should.equal "project_name" result.projectName.should.equal "project_name"
result.filePath.should.equal "/" result.filePath.should.equal "/"
describe 'updateProjectContents', ->
beforeEach ->
@UpdateMerger.mergeUpdate = sinon.stub().callsArg(3)
@req =
params:
0: @path = "chapters/main.tex"
project_id: @project_id = "project-id-123"
session:
destroy: sinon.stub()
@res =
send: sinon.stub()
@TpdsController.updateProjectContents @req, @res
it "should merge the update", ->
@UpdateMerger.mergeUpdate
.calledWith(@project_id, "/" + @path, @req)
.should.equal true
it "should return a success", ->
@res.send.calledWith(200).should.equal true
it "should clear the session", ->
@req.session.destroy.called.should.equal true
describe 'deleteProjectContents', ->
beforeEach ->
@UpdateMerger.deleteUpdate = sinon.stub().callsArg(2)
@req =
params:
0: @path = "chapters/main.tex"
project_id: @project_id = "project-id-123"
session:
destroy: sinon.stub()
@res =
send: sinon.stub()
@TpdsController.deleteProjectContents @req, @res
it "should delete the file", ->
@UpdateMerger.deleteUpdate
.calledWith(@project_id, "/" + @path)
.should.equal true
it "should return a success", ->
@res.send.calledWith(200).should.equal true
it "should clear the session", ->
@req.session.destroy.called.should.equal true