Merge branch 'master' into node-4.2
|
@ -7,18 +7,69 @@ module.exports = (grunt) ->
|
|||
grunt.loadNpmTasks 'grunt-mocha-test'
|
||||
grunt.loadNpmTasks 'grunt-available-tasks'
|
||||
grunt.loadNpmTasks 'grunt-contrib-requirejs'
|
||||
grunt.loadNpmTasks 'grunt-execute'
|
||||
grunt.loadNpmTasks 'grunt-bunyan'
|
||||
grunt.loadNpmTasks 'grunt-sed'
|
||||
grunt.loadNpmTasks 'grunt-git-rev-parse'
|
||||
grunt.loadNpmTasks 'grunt-file-append'
|
||||
grunt.loadNpmTasks 'grunt-file-append'
|
||||
grunt.loadNpmTasks 'grunt-env'
|
||||
grunt.loadNpmTasks 'grunt-newer'
|
||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||
grunt.loadNpmTasks 'grunt-parallel'
|
||||
grunt.loadNpmTasks 'grunt-exec'
|
||||
# grunt.loadNpmTasks 'grunt-contrib-imagemin'
|
||||
# grunt.loadNpmTasks 'grunt-sprity'
|
||||
|
||||
config =
|
||||
execute:
|
||||
app:
|
||||
src: "app.js"
|
||||
|
||||
exec:
|
||||
run:
|
||||
command:"node app.js | ./node_modules/logger-sharelatex/node_modules/bunyan/bin/bunyan --color"
|
||||
cssmin:
|
||||
command:"node_modules/clean-css/bin/cleancss --s0 -o public/stylesheets/style.css public/stylesheets/style.css"
|
||||
|
||||
|
||||
watch:
|
||||
coffee:
|
||||
files: 'public/**/*.coffee'
|
||||
tasks: ['quickcompile:coffee']
|
||||
options: {}
|
||||
|
||||
less:
|
||||
files: '**/*.less'
|
||||
tasks: ['compile:css']
|
||||
options: {}
|
||||
|
||||
|
||||
parallel:
|
||||
run:
|
||||
tasks:['exec', 'watch']
|
||||
options:
|
||||
grunt:true
|
||||
stream:true
|
||||
|
||||
|
||||
# imagemin:
|
||||
# dynamic:
|
||||
# files: [{
|
||||
# expand: true
|
||||
# cwd: 'public/img/'
|
||||
# src: ['**/*.{png,jpg,gif}']
|
||||
# dest: 'public/img/'
|
||||
# }]
|
||||
# options:
|
||||
# interlaced:false
|
||||
# optimizationLevel: 7
|
||||
|
||||
# sprity:
|
||||
# sprite:
|
||||
# options:
|
||||
# cssPath:"/img/"
|
||||
# 'style': '../../public/stylesheets/app/sprites.less'
|
||||
# margin: 0
|
||||
# src: ['./public/img/flags/24/*.png']
|
||||
# dest: './public/img/sprite'
|
||||
|
||||
|
||||
coffee:
|
||||
app_dir:
|
||||
|
@ -84,11 +135,16 @@ module.exports = (grunt) ->
|
|||
files:
|
||||
"public/stylesheets/style.css": "public/stylesheets/style.less"
|
||||
|
||||
|
||||
|
||||
|
||||
env:
|
||||
run:
|
||||
add:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED:0
|
||||
|
||||
|
||||
|
||||
requirejs:
|
||||
compile:
|
||||
options:
|
||||
|
@ -170,6 +226,9 @@ module.exports = (grunt) ->
|
|||
pattern: "@@RELEASE@@"
|
||||
replacement: process.env.BUILD_NUMBER || "(unknown build)"
|
||||
|
||||
|
||||
|
||||
|
||||
availabletasks:
|
||||
tasks:
|
||||
options:
|
||||
|
@ -304,12 +363,14 @@ module.exports = (grunt) ->
|
|||
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:minify', 'Concat and minify the client side js', ['requirejs', "file_append"]
|
||||
grunt.registerTask 'compile:minify', 'Concat and minify the client side js', ['requirejs', "file_append", "exec:cssmin",]
|
||||
grunt.registerTask 'compile:unit_tests', 'Compile the unit tests', ['clean:unit_tests', 'coffee:unit_tests']
|
||||
grunt.registerTask 'compile:acceptance_tests', 'Compile the acceptance tests', ['clean:acceptance_tests', 'coffee:acceptance_tests']
|
||||
grunt.registerTask 'compile:smoke_tests', 'Compile the smoke tests', ['coffee:smoke_tests']
|
||||
grunt.registerTask 'compile:tests', 'Compile all the tests', ['compile:smoke_tests', 'compile:unit_tests']
|
||||
grunt.registerTask 'compile', 'Compiles everything need to run web-sharelatex', ['compile:server', 'compile:client', 'compile:css']
|
||||
grunt.registerTask 'quickcompile:coffee', 'Compiles only changed coffee files',['newer:coffee']
|
||||
|
||||
|
||||
grunt.registerTask 'install', "Compile everything when installing as an npm module", ['compile']
|
||||
|
||||
|
@ -319,11 +380,13 @@ module.exports = (grunt) ->
|
|||
|
||||
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', 'env:run', 'execute']
|
||||
grunt.registerTask 'run', "Compile and run the web-sharelatex server", ['compile', 'env:run', 'parallel']
|
||||
|
||||
grunt.registerTask 'default', 'run'
|
||||
|
||||
grunt.registerTask 'version', "Write the version number into sentry.jade", ['git-rev-parse', 'sed']
|
||||
|
||||
|
||||
grunt.registerTask 'create-admin-user', "Create a user with the given email address and make them an admin. Update in place if the user already exists", () ->
|
||||
done = @async()
|
||||
email = grunt.option("email")
|
||||
|
@ -357,3 +420,23 @@ module.exports = (grunt) ->
|
|||
|
||||
"""
|
||||
done()
|
||||
|
||||
grunt.registerTask 'delete-user', "deletes a user and all their data", () ->
|
||||
done = @async()
|
||||
email = grunt.option("email")
|
||||
if !email?
|
||||
console.error "Usage: grunt delete-user --email joe@example.com"
|
||||
process.exit(1)
|
||||
settings = require "settings-sharelatex"
|
||||
UserGetter = require "./app/js/Features/User/UserGetter"
|
||||
UserDeleter = require "./app/js/Features/User/UserDeleter"
|
||||
UserGetter.getUser email:email, (error, user) ->
|
||||
if error?
|
||||
throw error
|
||||
if !user?
|
||||
console.log("user #{email} not in database, potentially already deleted")
|
||||
return done()
|
||||
UserDeleter.deleteUser user._id, (err)->
|
||||
if err?
|
||||
throw err
|
||||
done()
|
|
@ -6,9 +6,17 @@ web-sharelatex is the front-end web service of the open-source web-based collabo
|
|||
It serves all the HTML pages, CSS and javascript to the client. web-sharelatex also contains
|
||||
a lot of logic around creating and editing projects, and account management.
|
||||
|
||||
|
||||
The rest of the ShareLaTeX stack, along with information about contributing can be found in the
|
||||
[sharelatex/sharelatex](https://github.com/sharelatex/sharelatex) repository.
|
||||
|
||||
Build process
|
||||
----------------
|
||||
|
||||
web-sharelatex uses [Grunt](http://gruntjs.com/) to build its front-end related assets.
|
||||
|
||||
Image processing tasks are commented out in the gruntfile and the needed packages aren't presently in the project's `package.json`. If the images need to be processed again (minified and sprited), start by fetching the packages (`npm install grunt-contrib-imagemin grunt-sprity`), then *decomment* the tasks in `Gruntfile.coffee`. After this, the tasks can be called (explicitly, via `grunt imagemin` and `grunt sprity`).
|
||||
|
||||
Unit test status
|
||||
----------------
|
||||
|
||||
|
@ -42,3 +50,11 @@ in the `public/img/iconshock` directory found via
|
|||
[findicons.com](http://findicons.com/icon/498089/height?id=526085#)
|
||||
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
To run the Acceptance tests:
|
||||
|
||||
- set `allowPublicAccess` to true, either in the configuration file,
|
||||
or by setting the environment variable `SHARELATEX_ALLOW_PUBLIC_ACCESS` to `true`
|
||||
- start the server (`grunt`)
|
||||
- in a separate terminal, run `grunt test:acceptance`
|
||||
|
|
|
@ -9,6 +9,7 @@ Url = require("url")
|
|||
Settings = require "settings-sharelatex"
|
||||
basicAuth = require('basic-auth-connect')
|
||||
UserHandler = require("../User/UserHandler")
|
||||
UserSessionsManager = require("../User/UserSessionsManager")
|
||||
|
||||
module.exports = AuthenticationController =
|
||||
login: (req, res, next = (error) ->) ->
|
||||
|
@ -132,6 +133,8 @@ module.exports = AuthenticationController =
|
|||
isAdmin: user.isAdmin
|
||||
email: user.email
|
||||
referal_id: user.referal_id
|
||||
session_created: (new Date()).toISOString()
|
||||
ip_address: req.ip
|
||||
# Regenerate the session to get a new sessionID (cookie value) to
|
||||
# protect against session fixation attacks
|
||||
oldSession = req.session
|
||||
|
@ -141,4 +144,6 @@ module.exports = AuthenticationController =
|
|||
req.session[key] = value
|
||||
|
||||
req.session.user = lightUser
|
||||
|
||||
UserSessionsManager.trackSession(user, req.sessionID, () ->)
|
||||
callback()
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
Settings = require "settings-sharelatex"
|
||||
User = require("../../models/User").User
|
||||
{db, ObjectId} = require("../../infrastructure/mongojs")
|
||||
crypto = require 'crypto'
|
||||
bcrypt = require 'bcrypt'
|
||||
|
||||
BCRYPT_ROUNDS = Settings?.security?.bcryptRounds or 12
|
||||
|
||||
module.exports = AuthenticationManager =
|
||||
authenticate: (query, password, callback = (error, user) ->) ->
|
||||
# Using Mongoose for legacy reasons here. The returned User instance
|
||||
|
@ -15,6 +18,8 @@ module.exports = AuthenticationManager =
|
|||
bcrypt.compare password, user.hashedPassword, (error, match) ->
|
||||
return callback(error) if error?
|
||||
if match
|
||||
AuthenticationManager.checkRounds user, user.hashedPassword, password, (err) ->
|
||||
return callback(err) if err?
|
||||
callback null, user
|
||||
else
|
||||
callback null, null
|
||||
|
@ -24,7 +29,7 @@ module.exports = AuthenticationManager =
|
|||
callback null, null
|
||||
|
||||
setUserPassword: (user_id, password, callback = (error) ->) ->
|
||||
bcrypt.genSalt 7, (error, salt) ->
|
||||
bcrypt.genSalt BCRYPT_ROUNDS, (error, salt) ->
|
||||
return callback(error) if error?
|
||||
bcrypt.hash password, salt, (error, hash) ->
|
||||
return callback(error) if error?
|
||||
|
@ -35,3 +40,10 @@ module.exports = AuthenticationManager =
|
|||
$unset: password: true
|
||||
}, callback)
|
||||
|
||||
checkRounds: (user, hashedPassword, password, callback = (error) ->) ->
|
||||
# check current number of rounds and rehash if necessary
|
||||
currentRounds = bcrypt.getRounds hashedPassword
|
||||
if currentRounds < BCRYPT_ROUNDS
|
||||
AuthenticationManager.setUserPassword user._id, password, callback
|
||||
else
|
||||
callback()
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
BetaProgramHandler = require './BetaProgramHandler'
|
||||
UserLocator = require "../User/UserLocator"
|
||||
Settings = require "settings-sharelatex"
|
||||
logger = require 'logger-sharelatex'
|
||||
|
||||
|
||||
module.exports = BetaProgramController =
|
||||
|
||||
optIn: (req, res, next) ->
|
||||
user_id = req?.session?.user?._id
|
||||
logger.log {user_id}, "user opting in to beta program"
|
||||
if !user_id
|
||||
return next(new Error("no user id in session"))
|
||||
BetaProgramHandler.optIn user_id, (err) ->
|
||||
if err
|
||||
return next(err)
|
||||
return res.redirect "/beta/participate"
|
||||
|
||||
optOut: (req, res, next) ->
|
||||
user_id = req?.session?.user?._id
|
||||
logger.log {user_id}, "user opting out of beta program"
|
||||
if !user_id
|
||||
return next(new Error("no user id in session"))
|
||||
BetaProgramHandler.optOut user_id, (err) ->
|
||||
if err
|
||||
return next(err)
|
||||
return res.redirect "/beta/participate"
|
||||
|
||||
optInPage: (req, res, next)->
|
||||
user_id = req.session?.user?._id
|
||||
logger.log {user_id}, "showing beta participation page for user"
|
||||
UserLocator.findById user_id, (err, user)->
|
||||
if err
|
||||
logger.err {err, user_id}, "error fetching user"
|
||||
return next(err)
|
||||
res.render 'beta_program/opt_in',
|
||||
title:'sharelatex_beta_program'
|
||||
user: user,
|
||||
languages: Settings.languages,
|
|
@ -0,0 +1,31 @@
|
|||
User = require("../../models/User").User
|
||||
logger = require 'logger-sharelatex'
|
||||
metrics = require("../../infrastructure/Metrics")
|
||||
|
||||
module.exports = BetaProgramHandler =
|
||||
|
||||
optIn: (user_id, callback=(err)->) ->
|
||||
User.findById user_id, (err, user) ->
|
||||
if err
|
||||
logger.err {err, user_id}, "problem adding user to beta"
|
||||
return callback(err)
|
||||
metrics.inc "beta-program.opt-in"
|
||||
user.betaProgram = true
|
||||
user.save (err) ->
|
||||
if err
|
||||
logger.err {err, user_id}, "problem adding user to beta"
|
||||
return callback(err)
|
||||
return callback(null)
|
||||
|
||||
optOut: (user_id, callback=(err)->) ->
|
||||
User.findById user_id, (err, user) ->
|
||||
if err
|
||||
logger.err {err, user_id}, "problem removing user from beta"
|
||||
return callback(err)
|
||||
metrics.inc "beta-program.opt-out"
|
||||
user.betaProgram = false
|
||||
user.save (err) ->
|
||||
if err
|
||||
logger.err {err, user_id}, "problem removing user from beta"
|
||||
return callback(err)
|
||||
return callback(null)
|
|
@ -20,13 +20,15 @@ module.exports = BlogController =
|
|||
|
||||
logger.log url:url, "proxying request to blog api"
|
||||
request.get blogUrl, (err, r, data)->
|
||||
if r?.statusCode == 404
|
||||
if r?.statusCode == 404 or r?.statusCode == 403
|
||||
return ErrorController.notFound(req, res, next)
|
||||
if err?
|
||||
return res.send 500
|
||||
data = data.trim()
|
||||
try
|
||||
data = JSON.parse(data)
|
||||
if settings.cdn?.web?.host?
|
||||
data?.content = data?.content?.replace(/src="([^"]+)"/g, "src='#{settings.cdn?.web?.host}$1'");
|
||||
catch err
|
||||
logger.err err:err, data:data, "error parsing data from data"
|
||||
res.render "blog/blog_holder", data
|
||||
|
|
|
@ -30,12 +30,17 @@ module.exports = CollaboratorsHandler =
|
|||
getMembersWithPrivilegeLevels: (project_id, callback = (error, members) ->) ->
|
||||
CollaboratorsHandler.getMemberIdsWithPrivilegeLevels project_id, (error, members = []) ->
|
||||
return callback(error) if error?
|
||||
result = []
|
||||
async.mapLimit members, 3,
|
||||
(member, cb) ->
|
||||
UserGetter.getUser member.id, (error, user) ->
|
||||
return cb(error) if error?
|
||||
return cb(null, { user: user, privilegeLevel: member.privilegeLevel })
|
||||
callback
|
||||
if user?
|
||||
result.push { user: user, privilegeLevel: member.privilegeLevel }
|
||||
cb()
|
||||
(error) ->
|
||||
return callback(error) if error?
|
||||
callback null, result
|
||||
|
||||
getMemberIdPrivilegeLevel: (user_id, project_id, callback = (error, privilegeLevel) ->) ->
|
||||
# In future if the schema changes and getting all member ids is more expensive (multiple documents)
|
||||
|
@ -82,6 +87,18 @@ module.exports = CollaboratorsHandler =
|
|||
logger.error err: err, "problem removing user from project collaberators"
|
||||
callback(err)
|
||||
|
||||
removeUserFromAllProjets: (user_id, callback = (error) ->) ->
|
||||
CollaboratorsHandler.getProjectsUserIsCollaboratorOf user_id, { _id: 1 }, (error, readAndWriteProjects = [], readOnlyProjects = []) ->
|
||||
return callback(error) if error?
|
||||
allProjects = readAndWriteProjects.concat(readOnlyProjects)
|
||||
jobs = []
|
||||
for project in allProjects
|
||||
do (project) ->
|
||||
jobs.push (cb) ->
|
||||
return cb() if !project?
|
||||
CollaboratorsHandler.removeUserFromProject project._id, user_id, cb
|
||||
async.series jobs, callback
|
||||
|
||||
addEmailToProject: (project_id, adding_user_id, unparsed_email, privilegeLevel, callback = (error, user) ->) ->
|
||||
emails = mimelib.parseAddresses(unparsed_email)
|
||||
email = emails[0]?.address?.toLowerCase()
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
_ = require("lodash")
|
||||
async = require("async")
|
||||
|
||||
module.exports = ClsiFormatChecker =
|
||||
|
||||
checkRecoursesForProblems: (resources, callback)->
|
||||
jobs =
|
||||
conflictedPaths: (cb)->
|
||||
ClsiFormatChecker._checkForConflictingPaths resources, cb
|
||||
|
||||
sizeCheck: (cb)->
|
||||
ClsiFormatChecker._checkDocsAreUnderSizeLimit resources, cb
|
||||
|
||||
async.series jobs, (err, problems)->
|
||||
if err?
|
||||
return callback(err)
|
||||
|
||||
problems = _.omitBy(problems, _.isEmpty)
|
||||
|
||||
if _.isEmpty(problems)
|
||||
return callback()
|
||||
else
|
||||
callback(null, problems)
|
||||
|
||||
|
||||
_checkForConflictingPaths: (resources, callback)->
|
||||
paths = _.map(resources, 'path')
|
||||
|
||||
conflicts = _.filter paths, (path)->
|
||||
matchingPaths = _.filter paths, (checkPath)->
|
||||
return checkPath.indexOf(path+"/") != -1
|
||||
|
||||
return matchingPaths.length > 0
|
||||
|
||||
conflictObjects = _.map conflicts, (conflict)->
|
||||
path:conflict
|
||||
|
||||
callback null, conflictObjects
|
||||
|
||||
_checkDocsAreUnderSizeLimit: (resources, callback)->
|
||||
|
||||
FIVEMB = 1000 * 1000 * 5
|
||||
|
||||
totalSize = 0
|
||||
|
||||
sizedResources = _.map resources, (resource)->
|
||||
result = {path:resource.path}
|
||||
if resource.content?
|
||||
result.size = resource.content.replace(/\n/g).length
|
||||
result.kbSize = Math.ceil(result.size / 1000)
|
||||
else
|
||||
result.size = 0
|
||||
totalSize += result.size
|
||||
return result
|
||||
|
||||
tooLarge = totalSize > FIVEMB
|
||||
if !tooLarge
|
||||
return callback()
|
||||
else
|
||||
sizedResources = _.sortBy(sizedResources, "size").reverse().slice(0, 10)
|
||||
return callback(null, {resources:sizedResources, totalSize:totalSize})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -7,15 +7,24 @@ ProjectEntityHandler = require("../Project/ProjectEntityHandler")
|
|||
logger = require "logger-sharelatex"
|
||||
Url = require("url")
|
||||
ClsiCookieManager = require("./ClsiCookieManager")
|
||||
|
||||
_ = require("underscore")
|
||||
async = require("async")
|
||||
ClsiFormatChecker = require("./ClsiFormatChecker")
|
||||
|
||||
module.exports = ClsiManager =
|
||||
|
||||
sendRequest: (project_id, options = {}, callback = (error, success) ->) ->
|
||||
sendRequest: (project_id, user_id, options = {}, callback = (error, status, outputFiles, clsiServerId, validationProblems) ->) ->
|
||||
ClsiManager._buildRequest project_id, options, (error, req) ->
|
||||
return callback(error) if error?
|
||||
logger.log project_id: project_id, "sending compile to CLSI"
|
||||
ClsiManager._postToClsi project_id, req, options.compileGroup, (error, response) ->
|
||||
ClsiFormatChecker.checkRecoursesForProblems req.compile?.resources, (err, validationProblems)->
|
||||
if err?
|
||||
logger.err err, project_id, "could not check resources for potential problems before sending to clsi"
|
||||
return callback(err)
|
||||
if validationProblems?
|
||||
logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
|
||||
return callback(null, "validation-problems", null, null, validationProblems)
|
||||
ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
|
||||
if error?
|
||||
logger.err err:error, project_id:project_id, "error sending request to clsi"
|
||||
return callback(error)
|
||||
|
@ -27,13 +36,19 @@ module.exports = ClsiManager =
|
|||
outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
|
||||
callback(null, response?.compile?.status, outputFiles, clsiServerId)
|
||||
|
||||
deleteAuxFiles: (project_id, options, callback = (error) ->) ->
|
||||
compilerUrl = @_getCompilerUrl(options?.compileGroup)
|
||||
stopCompile: (project_id, user_id, options, callback = (error) ->) ->
|
||||
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")
|
||||
opts =
|
||||
url:"#{compilerUrl}/project/#{project_id}"
|
||||
method:"DELETE"
|
||||
url:compilerUrl
|
||||
method:"POST"
|
||||
ClsiManager._makeRequest project_id, opts, callback
|
||||
|
||||
deleteAuxFiles: (project_id, user_id, options, callback = (error) ->) ->
|
||||
compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id)
|
||||
opts =
|
||||
url:compilerUrl
|
||||
method:"DELETE"
|
||||
ClsiManager._makeRequest project_id, opts, callback
|
||||
|
||||
_makeRequest: (project_id, opts, callback)->
|
||||
ClsiCookieManager.getCookieJar project_id, (err, jar)->
|
||||
|
@ -51,14 +66,17 @@ module.exports = ClsiManager =
|
|||
|
||||
return callback err, response, body
|
||||
|
||||
_getCompilerUrl: (compileGroup, project_id, user_id, action) ->
|
||||
host = Settings.apis.clsi.url
|
||||
path = "/project/#{project_id}"
|
||||
path += "/user/#{user_id}" if user_id?
|
||||
path += "/#{action}" if action?
|
||||
return "#{host}#{path}"
|
||||
|
||||
_getCompilerUrl: (compileGroup) ->
|
||||
return Settings.apis.clsi.url
|
||||
|
||||
_postToClsi: (project_id, req, compileGroup, callback = (error, response) ->) ->
|
||||
compilerUrl = Settings.apis.clsi.url
|
||||
_postToClsi: (project_id, user_id, req, compileGroup, callback = (error, response) ->) ->
|
||||
compileUrl = @_getCompilerUrl(compileGroup, project_id, user_id, "compile")
|
||||
opts =
|
||||
url: "#{compilerUrl}/project/#{project_id}/compile"
|
||||
url: compileUrl
|
||||
json: req
|
||||
method: "POST"
|
||||
ClsiManager._makeRequest project_id, opts, (error, response, body) ->
|
||||
|
@ -75,10 +93,9 @@ module.exports = ClsiManager =
|
|||
_parseOutputFiles: (project_id, rawOutputFiles = []) ->
|
||||
outputFiles = []
|
||||
for file in rawOutputFiles
|
||||
path = Url.parse(file.url).path
|
||||
path = path.replace("/project/#{project_id}/output/", "")
|
||||
outputFiles.push
|
||||
path: path
|
||||
path: file.path # the clsi is now sending this to web
|
||||
url: Url.parse(file.url).path # the location of the file on the clsi, excluding the host part
|
||||
type: file.type
|
||||
build: file.build
|
||||
return outputFiles
|
||||
|
@ -134,15 +151,15 @@ module.exports = ClsiManager =
|
|||
resources: resources
|
||||
}
|
||||
|
||||
wordCount: (project_id, file, options, callback = (error, response) ->) ->
|
||||
wordCount: (project_id, user_id, file, options, callback = (error, response) ->) ->
|
||||
ClsiManager._buildRequest project_id, options, (error, req) ->
|
||||
compilerUrl = ClsiManager._getCompilerUrl(options?.compileGroup)
|
||||
filename = file || req?.compile?.rootResourcePath
|
||||
wordcount_url = "#{compilerUrl}/project/#{project_id}/wordcount?file=#{encodeURIComponent(filename)}"
|
||||
if req.compile.options.imageName?
|
||||
wordcount_url += "&image=#{encodeURIComponent(req.compile.options.imageName)}"
|
||||
wordcount_url = ClsiManager._getCompilerUrl(options?.compileGroup, project_id, user_id, "wordcount")
|
||||
opts =
|
||||
url: wordcount_url
|
||||
qs:
|
||||
file: filename
|
||||
image: req.compile.options.imageName
|
||||
method: "GET"
|
||||
ClsiManager._makeRequest project_id, opts, (error, response, body) ->
|
||||
return callback(error) if error?
|
||||
|
|
|
@ -29,8 +29,8 @@ module.exports = CompileController =
|
|||
options.compiler = req.body.compiler
|
||||
if req.body?.draft
|
||||
options.draft = req.body.draft
|
||||
logger.log {options, project_id}, "got compile request"
|
||||
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits) ->
|
||||
logger.log {options:options, project_id:project_id, user_id:user_id}, "got compile request"
|
||||
CompileManager.compile project_id, user_id, options, (error, status, outputFiles, clsiServerId, limits, validationProblems) ->
|
||||
return next(error) if error?
|
||||
res.contentType("application/json")
|
||||
res.status(200).send JSON.stringify {
|
||||
|
@ -38,8 +38,32 @@ module.exports = CompileController =
|
|||
outputFiles: outputFiles
|
||||
compileGroup: limits?.compileGroup
|
||||
clsiServerId:clsiServerId
|
||||
validationProblems:validationProblems
|
||||
}
|
||||
|
||||
stopCompile: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
AuthenticationController.getLoggedInUserId req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
logger.log {project_id:project_id, user_id:user_id}, "stop compile request"
|
||||
CompileManager.stopCompile project_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.status(200).send()
|
||||
|
||||
_compileAsUser: (req, callback) ->
|
||||
# callback with user_id if per-user, undefined otherwise
|
||||
if not Settings.disablePerUserCompiles
|
||||
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
|
||||
else
|
||||
callback() # do a per-project compile, not per-user
|
||||
|
||||
_downloadAsUser: (req, callback) ->
|
||||
# callback with user_id if per-user, undefined otherwise
|
||||
if not Settings.disablePerUserCompiles
|
||||
AuthenticationController.getLoggedInUserId req, callback # -> (error, user_id)
|
||||
else
|
||||
callback() # do a per-project compile, not per-user
|
||||
|
||||
downloadPdf: (req, res, next = (error) ->)->
|
||||
Metrics.inc "pdf-downloads"
|
||||
project_id = req.params.Project_id
|
||||
|
@ -72,16 +96,22 @@ module.exports = CompileController =
|
|||
logger.log project_id:project_id, ip:req.ip, "rate limit hit downloading pdf"
|
||||
res.send 500
|
||||
else
|
||||
CompileController.proxyToClsi(project_id, "/project/#{project_id}/output/output.pdf", req, res, next)
|
||||
CompileController._downloadAsUser req, (error, user_id) ->
|
||||
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, "output.pdf"
|
||||
CompileController.proxyToClsi(project_id, url, req, res, next)
|
||||
|
||||
deleteAuxFiles: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
CompileManager.deleteAuxFiles project_id, (error) ->
|
||||
CompileController._compileAsUser req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
CompileManager.deleteAuxFiles project_id, user_id, (error) ->
|
||||
return next(error) if error?
|
||||
res.sendStatus(200)
|
||||
|
||||
# this is only used by templates, so is not called with a user_id
|
||||
compileAndDownloadPdf: (req, res, next)->
|
||||
project_id = req.params.project_id
|
||||
# pass user_id as null, since templates are an "anonymous" compile
|
||||
CompileManager.compile project_id, null, {}, (err)->
|
||||
if err?
|
||||
logger.err err:err, project_id:project_id, "something went wrong compile and downloading pdf"
|
||||
|
@ -91,13 +121,29 @@ module.exports = CompileController =
|
|||
|
||||
getFileFromClsi: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
build = req.params.build
|
||||
if build?
|
||||
url = "/project/#{project_id}/build/#{build}/output/#{req.params.file}"
|
||||
else
|
||||
url = "/project/#{project_id}/output/#{req.params.file}"
|
||||
CompileController._downloadAsUser req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
url = CompileController._getFileUrl project_id, user_id, req.params.build_id, req.params.file
|
||||
CompileController.proxyToClsi(project_id, url, req, res, next)
|
||||
|
||||
# compute a GET file url for a given project, user (optional), build (optional) and file
|
||||
_getFileUrl: (project_id, user_id, build_id, file) ->
|
||||
if user_id? and build_id?
|
||||
url = "/project/#{project_id}/user/#{user_id}/build/#{build_id}/output/#{file}"
|
||||
else if user_id?
|
||||
url = "/project/#{project_id}/user/#{user_id}/output/#{file}"
|
||||
else if build_id?
|
||||
url = "/project/#{project_id}/build/#{build_id}/output/#{file}"
|
||||
else
|
||||
url = "/project/#{project_id}/output/#{file}"
|
||||
return url
|
||||
|
||||
# compute a POST url for a project, user (optional) and action
|
||||
_getUrl: (project_id, user_id, action) ->
|
||||
path = "/project/#{project_id}"
|
||||
path += "/user/#{user_id}" if user_id?
|
||||
return "#{path}/#{action}"
|
||||
|
||||
proxySyncPdf: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
{page, h, v} = req.query
|
||||
|
@ -107,19 +153,34 @@ module.exports = CompileController =
|
|||
return next(new Error("invalid h parameter"))
|
||||
if not v?.match(/^\d+\.\d+$/)
|
||||
return next(new Error("invalid v parameter"))
|
||||
destination = {url: "/project/#{project_id}/sync/pdf", qs: {page, h, v}}
|
||||
# whether this request is going to a per-user container
|
||||
CompileController._compileAsUser req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
url = CompileController._getUrl(project_id, user_id, "sync/pdf")
|
||||
destination = {url: url, qs: {page, h, v}}
|
||||
CompileController.proxyToClsi(project_id, destination, req, res, next)
|
||||
|
||||
proxySyncCode: (req, res, next = (error) ->) ->
|
||||
project_id = req.params.Project_id
|
||||
{file, line, column} = req.query
|
||||
if not file? or Path.resolve("/", file) isnt "/#{file}"
|
||||
if not file?
|
||||
return next(new Error("missing file parameter"))
|
||||
# Check that we are dealing with a simple file path (this is not
|
||||
# strictly needed because synctex uses this parameter as a label
|
||||
# to look up in the synctex output, and does not open the file
|
||||
# itself). Since we have valid synctex paths like foo/./bar we
|
||||
# allow those by replacing /./ with /
|
||||
testPath = file.replace '/./', '/'
|
||||
if Path.resolve("/", testPath) isnt "/#{testPath}"
|
||||
return next(new Error("invalid file parameter"))
|
||||
if not line?.match(/^\d+$/)
|
||||
return next(new Error("invalid line parameter"))
|
||||
if not column?.match(/^\d+$/)
|
||||
return next(new Error("invalid column parameter"))
|
||||
destination = {url:"/project/#{project_id}/sync/code", qs: {file, line, column}}
|
||||
CompileController._compileAsUser req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
url = CompileController._getUrl(project_id, user_id, "sync/code")
|
||||
destination = {url:url, qs: {file, line, column}}
|
||||
CompileController.proxyToClsi(project_id, destination, req, res, next)
|
||||
|
||||
proxyToClsi: (project_id, url, req, res, next = (error) ->) ->
|
||||
|
@ -138,9 +199,6 @@ module.exports = CompileController =
|
|||
# expand any url parameter passed in as {url:..., qs:...}
|
||||
if typeof url is "object"
|
||||
{url, qs} = url
|
||||
if limits.compileGroup == "priority"
|
||||
compilerUrl = Settings.apis.clsi_priority.url
|
||||
else
|
||||
compilerUrl = Settings.apis.clsi.url
|
||||
url = "#{compilerUrl}#{url}"
|
||||
logger.log url: url, "proxying to CLSI"
|
||||
|
@ -168,7 +226,9 @@ module.exports = CompileController =
|
|||
wordCount: (req, res, next) ->
|
||||
project_id = req.params.Project_id
|
||||
file = req.query.file || false
|
||||
CompileManager.wordCount project_id, file, (error, body) ->
|
||||
CompileController._compileAsUser req, (error, user_id) ->
|
||||
return next(error) if error?
|
||||
CompileManager.wordCount project_id, user_id, file, (error, body) ->
|
||||
return next(error) if error?
|
||||
res.contentType("application/json")
|
||||
res.send body
|
||||
|
|
|
@ -37,15 +37,23 @@ module.exports = CompileManager =
|
|||
return callback(error) if error?
|
||||
for key, value of limits
|
||||
options[key] = value
|
||||
ClsiManager.sendRequest project_id, options, (error, status, outputFiles, clsiServerId) ->
|
||||
# only pass user_id down to clsi if this is a per-user compile
|
||||
compileAsUser = if Settings.disablePerUserCompiles then undefined else user_id
|
||||
ClsiManager.sendRequest project_id, compileAsUser, options, (error, status, outputFiles, clsiServerId, validationProblems) ->
|
||||
return callback(error) if error?
|
||||
logger.log files: outputFiles, "output files"
|
||||
callback(null, status, outputFiles, clsiServerId, limits)
|
||||
callback(null, status, outputFiles, clsiServerId, limits, validationProblems)
|
||||
|
||||
deleteAuxFiles: (project_id, callback = (error) ->) ->
|
||||
|
||||
stopCompile: (project_id, user_id, callback = (error) ->) ->
|
||||
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
|
||||
return callback(error) if error?
|
||||
ClsiManager.deleteAuxFiles project_id, limits, callback
|
||||
ClsiManager.stopCompile project_id, user_id, limits, callback
|
||||
|
||||
deleteAuxFiles: (project_id, user_id, callback = (error) ->) ->
|
||||
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
|
||||
return callback(error) if error?
|
||||
ClsiManager.deleteAuxFiles project_id, user_id, limits, callback
|
||||
|
||||
getProjectCompileLimits: (project_id, callback = (error, limits) ->) ->
|
||||
Project.findById project_id, {owner_ref: 1}, (error, project) ->
|
||||
|
@ -92,7 +100,7 @@ module.exports = CompileManager =
|
|||
else
|
||||
ProjectRootDocManager.setRootDocAutomatically project_id, callback
|
||||
|
||||
wordCount: (project_id, file, callback = (error) ->) ->
|
||||
wordCount: (project_id, user_id, file, callback = (error) ->) ->
|
||||
CompileManager.getProjectCompileLimits project_id, (error, limits) ->
|
||||
return callback(error) if error?
|
||||
ClsiManager.wordCount project_id, file, limits, callback
|
||||
ClsiManager.wordCount project_id, user_id, file, limits, callback
|
||||
|
|
|
@ -3,6 +3,8 @@ PersonalEmailLayout = require("./Layouts/PersonalEmailLayout")
|
|||
NotificationEmailLayout = require("./Layouts/NotificationEmailLayout")
|
||||
settings = require("settings-sharelatex")
|
||||
|
||||
|
||||
|
||||
templates = {}
|
||||
|
||||
templates.registered =
|
||||
|
@ -114,6 +116,8 @@ module.exports =
|
|||
template = templates[templateName]
|
||||
opts.siteUrl = settings.siteUrl
|
||||
opts.body = template.compiledTemplate(opts)
|
||||
if settings.email?.templates?.customFooter?
|
||||
opts.body += settings.email?.templates?.customFooter
|
||||
return {
|
||||
subject : template.subject(opts)
|
||||
html: template.layout(opts)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
|
||||
logger = require("logger-sharelatex")
|
||||
NotificationsHandler = require("./NotificationsHandler")
|
||||
|
||||
module.exports =
|
||||
|
||||
groupPlan: (user, licence)->
|
||||
key : "join-sub-#{licence.subscription_id}"
|
||||
|
||||
create: (callback = ->)->
|
||||
messageOpts =
|
||||
groupName: licence.name
|
||||
subscription_id: licence.subscription_id
|
||||
logger.log user_id:user._id, key:key, "creating notification key for user"
|
||||
NotificationsHandler.createNotification user._id, @key, "notification_group_invite", messageOpts, callback
|
||||
|
||||
read: (callback = ->)->
|
||||
|
|
|
@ -3,14 +3,22 @@ request = require("request")
|
|||
logger = require("logger-sharelatex")
|
||||
|
||||
oneSecond = 1000
|
||||
|
||||
makeRequest = (opts, callback)->
|
||||
if !settings.apis.notifications?.url?
|
||||
return callback(null, statusCode:200)
|
||||
else
|
||||
request(opts, callback)
|
||||
|
||||
module.exports =
|
||||
|
||||
getUserNotifications: (user_id, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
json: true
|
||||
timeout: oneSecond
|
||||
request.get opts, (err, res, unreadNotifications)->
|
||||
method: "GET"
|
||||
makeRequest opts, (err, res, unreadNotifications)->
|
||||
statusCode = if res? then res.statusCode else 500
|
||||
if err? or statusCode != 200
|
||||
e = new Error("something went wrong getting notifications, #{err}, #{statusCode}")
|
||||
|
@ -23,30 +31,33 @@ module.exports =
|
|||
|
||||
createNotification: (user_id, key, templateKey, messageOpts, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
timeout: oneSecond
|
||||
method:"POST"
|
||||
json: {
|
||||
key:key
|
||||
messageOpts:messageOpts
|
||||
templateKey:templateKey
|
||||
}
|
||||
logger.log opts:opts, "creating notification for user"
|
||||
request.post opts, callback
|
||||
makeRequest opts, callback
|
||||
|
||||
markAsReadWithKey: (user_id, key, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications.url}/user/#{user_id}"
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}"
|
||||
method: "DELETE"
|
||||
timeout: oneSecond
|
||||
json: {
|
||||
key:key
|
||||
}
|
||||
logger.log user_id:user_id, key:key, "sending mark notification as read with key to notifications api"
|
||||
request.del opts, callback
|
||||
makeRequest opts, callback
|
||||
|
||||
|
||||
markAsRead: (user_id, notification_id, callback)->
|
||||
opts =
|
||||
uri: "#{settings.apis.notifications.url}/user/#{user_id}/notification/#{notification_id}"
|
||||
method: "DELETE"
|
||||
uri: "#{settings.apis.notifications?.url}/user/#{user_id}/notification/#{notification_id}"
|
||||
timeout:oneSecond
|
||||
logger.log user_id:user_id, notification_id:notification_id, "sending mark notification as read to notifications api"
|
||||
request.del opts, callback
|
||||
makeRequest opts, callback
|
||||
|
|
|
@ -2,6 +2,7 @@ PasswordResetHandler = require("./PasswordResetHandler")
|
|||
RateLimiter = require("../../infrastructure/RateLimiter")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
UserGetter = require("../User/UserGetter")
|
||||
UserSessionsManager = require("../User/UserSessionsManager")
|
||||
logger = require "logger-sharelatex"
|
||||
|
||||
module.exports =
|
||||
|
@ -47,6 +48,8 @@ module.exports =
|
|||
PasswordResetHandler.setNewUserPassword passwordResetToken?.trim(), password?.trim(), (err, found, user_id) ->
|
||||
return next(err) if err?
|
||||
if found
|
||||
UserSessionsManager.revokeAllUserSessions {_id: user_id}, [], (err) ->
|
||||
return next(err) if err?
|
||||
if req.body.login_after
|
||||
UserGetter.getUser user_id, {email: 1}, (err, user) ->
|
||||
return next(err) if err?
|
||||
|
|
|
@ -245,11 +245,13 @@ module.exports = ProjectController =
|
|||
first_name : user.first_name
|
||||
last_name : user.last_name
|
||||
referal_id : user.referal_id
|
||||
signUpDate : user.signUpDate
|
||||
subscription :
|
||||
freeTrial: {allowed: allowedFreeTrial}
|
||||
featureSwitches: user.featureSwitches
|
||||
features: user.features
|
||||
refProviders: user.refProviders
|
||||
betaProgram: user.betaProgram
|
||||
}
|
||||
userSettings: {
|
||||
mode : user.ace.mode
|
||||
|
|
|
@ -24,9 +24,11 @@ module.exports = ProjectDeleter =
|
|||
update = {deletedByExternalDataSource: false}
|
||||
Project.update conditions, update, {}, callback
|
||||
|
||||
deleteUsersProjects: (owner_id, callback)->
|
||||
logger.log owner_id:owner_id, "deleting users projects"
|
||||
Project.remove owner_ref:owner_id, callback
|
||||
deleteUsersProjects: (user_id, callback)->
|
||||
logger.log {user_id}, "deleting users projects"
|
||||
Project.remove owner_ref:user_id, (error) ->
|
||||
return callback(error) if error?
|
||||
CollaboratorsHandler.removeUserFromAllProjets user_id, callback
|
||||
|
||||
deleteProject: (project_id, callback = (error) ->) ->
|
||||
# archiveProject takes care of the clean-up
|
||||
|
|
|
@ -512,6 +512,11 @@ module.exports = ProjectEntityHandler =
|
|||
return callback(e)
|
||||
type = sanitizeTypeOfElement type
|
||||
|
||||
if path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/")
|
||||
e = new Error("invalid element name")
|
||||
logger.err project_id:project._id, folder_id:folder_id, element:element, type:type, "failed trying to insert element as name was invalid"
|
||||
return callback(e)
|
||||
|
||||
if !folder_id?
|
||||
folder_id = project.rootFolder[0]._id
|
||||
ProjectEntityHandler._countElements project, (err, count)->
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports = ReferencesController =
|
|||
return res.sendStatus 400
|
||||
logger.log {projectId, docIds: docIds}, "index references for project"
|
||||
ReferencesHandler.index projectId, docIds, (err, data) ->
|
||||
if err
|
||||
if err?
|
||||
logger.err {err, projectId}, "error indexing all references"
|
||||
return res.sendStatus 500
|
||||
ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
|
||||
|
@ -25,12 +25,14 @@ module.exports = ReferencesController =
|
|||
shouldBroadcast = req.body.shouldBroadcast
|
||||
logger.log {projectId}, "index all references for project"
|
||||
ReferencesHandler.indexAll projectId, (err, data) ->
|
||||
if err
|
||||
if err?
|
||||
logger.err {err, projectId}, "error indexing all references"
|
||||
return res.sendStatus 500
|
||||
ReferencesController._handleIndexResponse(req, res, projectId, shouldBroadcast, data)
|
||||
|
||||
_handleIndexResponse: (req, res, projectId, shouldBroadcast, data) ->
|
||||
if !data? or !data.keys?
|
||||
return res.json({projectId, keys: []})
|
||||
if shouldBroadcast
|
||||
logger.log {projectId}, "emitting new references keys to connected clients"
|
||||
EditorRealTimeController.emitToRoom projectId, 'references:keys:updated', data.keys
|
||||
|
|
|
@ -66,6 +66,8 @@ module.exports = ReferencesHandler =
|
|||
ReferencesHandler._doIndexOperation(projectId, project, docIds, [], callback)
|
||||
|
||||
_doIndexOperation: (projectId, project, docIds, fileIds, callback) ->
|
||||
if !settings.apis?.references?.url?
|
||||
return callback()
|
||||
ReferencesHandler._isFullIndex project, (err, isFullIndex) ->
|
||||
if err
|
||||
logger.err {err, projectId}, "error checking whether to do full index"
|
||||
|
|
|
@ -5,6 +5,12 @@ _ = require("underscore")
|
|||
ErrorController = require "../Errors/ErrorController"
|
||||
StaticPageHelpers = require("./StaticPageHelpers")
|
||||
sanitize = require('sanitizer')
|
||||
Settings = require("settings-sharelatex")
|
||||
contentful = require('contentful')
|
||||
marked = require("marked")
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = UniversityController =
|
||||
|
||||
|
@ -17,7 +23,7 @@ module.exports = UniversityController =
|
|||
logger.log url:url, "proxying request to university api"
|
||||
request.get universityUrl, (err, r, data)->
|
||||
if r?.statusCode == 404
|
||||
return ErrorController.notFound(req, res, next)
|
||||
return UniversityController.getContentfulPage(req, res, next)
|
||||
if err?
|
||||
return res.send 500
|
||||
data = data.trim()
|
||||
|
@ -38,3 +44,27 @@ module.exports = UniversityController =
|
|||
upstream.on "error", (error) ->
|
||||
logger.error err: error, "university proxy error"
|
||||
upstream.pipe res
|
||||
|
||||
getContentfulPage: (req, res, next)->
|
||||
console.log Settings.contentful
|
||||
if !Settings.contentful?.uni?.space? and !Settings.contentful?.uni?.accessToken?
|
||||
return ErrorController.notFound(req, res, next)
|
||||
|
||||
client = contentful.createClient({
|
||||
space: Settings.contentful?.uni?.space
|
||||
accessToken: Settings.contentful?.uni?.accessToken
|
||||
})
|
||||
|
||||
url = req.url?.toLowerCase().replace("/university/","")
|
||||
client.getEntries({content_type: 'caseStudy', 'fields.slug':url})
|
||||
.catch (e)->
|
||||
return res.send 500
|
||||
.then (entry)->
|
||||
if !entry? or !entry.items? or entry.items.length == 0
|
||||
return ErrorController.notFound(req, res, next)
|
||||
viewData = entry.items[0].fields
|
||||
viewData.html = marked(viewData.content)
|
||||
res.render "university/case_study", viewData:viewData
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -38,12 +38,12 @@ module.exports =
|
|||
SubscriptionLocator.getUsersSubscription user._id, (err, subscription)->
|
||||
if err?
|
||||
return callback(err)
|
||||
hasValidSubscription = subscription? and subscription.recurlySubscription_id?
|
||||
hasValidSubscription = subscription? and (subscription.recurlySubscription_id? or subscription?.customAccount == true)
|
||||
logger.log user:user, hasValidSubscription:hasValidSubscription, subscription:subscription, "checking if user has subscription"
|
||||
callback err, hasValidSubscription, subscription
|
||||
|
||||
userIsMemberOfGroupSubscription: (user, callback = (error, isMember, subscriptions) ->) ->
|
||||
logger.log user_id: user._ud, "checking is user is member of subscription groups"
|
||||
logger.log user_id: user._id, "checking is user is member of subscription groups"
|
||||
SubscriptionLocator.getMemberSubscriptions user._id, (err, subscriptions = []) ->
|
||||
return callback(err) if err?
|
||||
callback err, subscriptions.length > 0, subscriptions
|
||||
|
|
|
@ -4,11 +4,201 @@ request = require 'request'
|
|||
Settings = require "settings-sharelatex"
|
||||
xml2js = require "xml2js"
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
|
||||
module.exports = RecurlyWrapper =
|
||||
apiUrl : "https://api.recurly.com/v2"
|
||||
|
||||
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
|
||||
_addressToXml: (address) ->
|
||||
allowedKeys = ['address1', 'address2', 'city', 'country', 'state', 'zip', 'postal_code']
|
||||
resultString = "<billing_info>\n"
|
||||
for k, v of address
|
||||
if k == 'postal_code'
|
||||
k = 'zip'
|
||||
if v and (k in allowedKeys)
|
||||
resultString += "<#{k}#{if k == 'address2' then ' nil="nil"' else ''}>#{v || ''}</#{k}>\n"
|
||||
resultString += "</billing_info>\n"
|
||||
return resultString
|
||||
|
||||
_paypal:
|
||||
checkAccountExists: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "checking if recurly account exists for user"
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{user._id}"
|
||||
method: "GET"
|
||||
}, (error, response, responseBody) ->
|
||||
if error
|
||||
if response.statusCode == 404 # actually not an error in this case, just no existing account
|
||||
cache.userExists = false
|
||||
return next(null, cache)
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while checking account"
|
||||
return next(error)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user appears to exist in recurly"
|
||||
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error parsing account"
|
||||
return next(err)
|
||||
cache.userExists = true
|
||||
cache.account = account
|
||||
return next(null, cache)
|
||||
)
|
||||
createAccount: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
address = subscriptionDetails.address
|
||||
if !address
|
||||
return next(new Error('no address in subscriptionDetails at createAccount stage'))
|
||||
if cache.userExists
|
||||
logger.log {user_id: user._id, recurly_token_id}, "user already exists in recurly"
|
||||
return next(null, cache)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating user in recurly"
|
||||
requestBody = """
|
||||
<account>
|
||||
<account_code>#{user._id}</account_code>
|
||||
<email>#{user.email}</email>
|
||||
<first_name>#{user.first_name}</first_name>
|
||||
<last_name>#{user.last_name}</last_name>
|
||||
<address>
|
||||
<address1>#{address.address1}</address1>
|
||||
<address2>#{address.address2}</address2>
|
||||
<city>#{address.city || ''}</city>
|
||||
<state>#{address.state || ''}</state>
|
||||
<zip>#{address.zip || ''}</zip>
|
||||
<country>#{address.country}</country>
|
||||
</address>
|
||||
</account>
|
||||
"""
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "accounts"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating account"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseAccountXml responseBody, (err, account) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error creating account"
|
||||
return next(err)
|
||||
cache.account = account
|
||||
return next(null, cache)
|
||||
)
|
||||
createBillingInfo: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating billing info in recurly"
|
||||
accountCode = cache?.account?.account_code
|
||||
if !accountCode
|
||||
return next(new Error('no account code at createBillingInfo stage'))
|
||||
requestBody = """
|
||||
<billing_info>
|
||||
<token_id>#{recurly_token_id}</token_id>
|
||||
</billing_info>
|
||||
"""
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountCode}/billing_info"
|
||||
method: "POST"
|
||||
body: requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating billing info"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, accountCode, recurly_token_id}, "error creating billing info"
|
||||
return next(err)
|
||||
cache.billingInfo = billingInfo
|
||||
return next(null, cache)
|
||||
)
|
||||
|
||||
setAddress: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "setting billing address in recurly"
|
||||
accountCode = cache?.account?.account_code
|
||||
if !accountCode
|
||||
return next(new Error('no account code at setAddress stage'))
|
||||
address = subscriptionDetails.address
|
||||
if !address
|
||||
return next(new Error('no address in subscriptionDetails at setAddress stage'))
|
||||
requestBody = RecurlyWrapper._addressToXml(address)
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountCode}/billing_info"
|
||||
method: "PUT"
|
||||
body: requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while setting address"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseBillingInfoXml responseBody, (err, billingInfo) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error updating billing info"
|
||||
return next(err)
|
||||
cache.billingInfo = billingInfo
|
||||
return next(null, cache)
|
||||
)
|
||||
createSubscription: (cache, next) ->
|
||||
user = cache.user
|
||||
recurly_token_id = cache.recurly_token_id
|
||||
subscriptionDetails = cache.subscriptionDetails
|
||||
logger.log {user_id: user._id, recurly_token_id}, "creating subscription in recurly"
|
||||
requestBody = """
|
||||
<subscription>
|
||||
<plan_code>#{subscriptionDetails.plan_code}</plan_code>
|
||||
<currency>#{subscriptionDetails.currencyCode}</currency>
|
||||
<coupon_code>#{subscriptionDetails.coupon_code}</coupon_code>
|
||||
<account>
|
||||
<account_code>#{user._id}</account_code>
|
||||
</account>
|
||||
</subscription>
|
||||
""" # TODO: check account details and billing
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
if error
|
||||
logger.error {error, user_id: user._id, recurly_token_id}, "error response from recurly while creating subscription"
|
||||
return next(error)
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, (err, subscription) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error creating subscription"
|
||||
return next(err)
|
||||
cache.subscription = subscription
|
||||
return next(null, cache)
|
||||
)
|
||||
|
||||
_createPaypalSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
|
||||
logger.log {user_id: user._id, recurly_token_id}, "starting process of creating paypal subscription"
|
||||
# We use `async.waterfall` to run each of these actions in sequence
|
||||
# passing a `cache` object along the way. The cache is initialized
|
||||
# with required data, and `async.apply` to pass the cache to the first function
|
||||
cache = {user, recurly_token_id, subscriptionDetails}
|
||||
Async.waterfall([
|
||||
Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache),
|
||||
RecurlyWrapper._paypal.createAccount,
|
||||
RecurlyWrapper._paypal.createBillingInfo,
|
||||
RecurlyWrapper._paypal.setAddress,
|
||||
RecurlyWrapper._paypal.createSubscription,
|
||||
], (err, result) ->
|
||||
if err
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
|
||||
return callback(err)
|
||||
if !result.subscription
|
||||
err = new Error('no subscription object in result')
|
||||
logger.error {err, user_id: user._id, recurly_token_id}, "error in paypal subscription creation process"
|
||||
return callback(err)
|
||||
logger.log {user_id: user._id, recurly_token_id}, "done creating paypal subscription for user"
|
||||
callback(null, result.subscription)
|
||||
)
|
||||
|
||||
_createCreditCardSubscription: (user, subscriptionDetails, recurly_token_id, callback) ->
|
||||
requestBody = """
|
||||
<subscription>
|
||||
<plan_code>#{subscriptionDetails.plan_code}</plan_code>
|
||||
|
@ -25,17 +215,23 @@ module.exports = RecurlyWrapper =
|
|||
</account>
|
||||
</subscription>
|
||||
"""
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions"
|
||||
method : "POST"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml responseBody, callback
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, callback
|
||||
)
|
||||
|
||||
createSubscription: (user, subscriptionDetails, recurly_token_id, callback)->
|
||||
isPaypal = subscriptionDetails.isPaypal
|
||||
logger.log {user_id: user._id, isPaypal, recurly_token_id}, "setting up subscription in recurly"
|
||||
fn = if isPaypal then RecurlyWrapper._createPaypalSubscription else RecurlyWrapper._createCreditCardSubscription
|
||||
fn user, subscriptionDetails, recurly_token_id, callback
|
||||
|
||||
apiRequest : (options, callback) ->
|
||||
options.url = @apiUrl + "/" + options.url
|
||||
options.url = RecurlyWrapper.apiUrl + "/" + options.url
|
||||
options.headers =
|
||||
"Authorization" : "Basic " + new Buffer(Settings.apis.recurly.apiKey).toString("base64")
|
||||
"Accept" : "application/xml"
|
||||
|
@ -77,11 +273,11 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
getSubscriptions: (accountId, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}/subscriptions"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
|
||||
|
@ -94,11 +290,11 @@ module.exports = RecurlyWrapper =
|
|||
else
|
||||
url = "subscriptions/#{subscriptionId}"
|
||||
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: url
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml body, (error, recurlySubscription) =>
|
||||
RecurlyWrapper._parseSubscriptionXml body, (error, recurlySubscription) =>
|
||||
return callback(error) if error?
|
||||
if options.includeAccount
|
||||
if recurlySubscription.account? and recurlySubscription.account.url?
|
||||
|
@ -106,7 +302,7 @@ module.exports = RecurlyWrapper =
|
|||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
|
||||
@getAccount accountId, (error, account) ->
|
||||
RecurlyWrapper.getAccount accountId, (error, account) ->
|
||||
return callback(error) if error?
|
||||
recurlySubscription.account = account
|
||||
callback null, recurlySubscription
|
||||
|
@ -124,9 +320,9 @@ module.exports = RecurlyWrapper =
|
|||
per_page:200
|
||||
if cursor?
|
||||
opts.qs.cursor = cursor
|
||||
@apiRequest opts, (error, response, body) =>
|
||||
RecurlyWrapper.apiRequest opts, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, (err, data)->
|
||||
RecurlyWrapper._parseXml body, (err, data)->
|
||||
if err?
|
||||
logger.err err:err, "could not get accoutns"
|
||||
callback(err)
|
||||
|
@ -142,19 +338,19 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
getAccount: (accountId, callback) ->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseAccountXml body, callback
|
||||
RecurlyWrapper._parseAccountXml body, callback
|
||||
)
|
||||
|
||||
getBillingInfo: (accountId, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "accounts/#{accountId}/billing_info"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
|
||||
|
@ -166,13 +362,13 @@ module.exports = RecurlyWrapper =
|
|||
<timeframe>#{options.timeframe}</timeframe>
|
||||
</subscription>
|
||||
"""
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "subscriptions/#{subscriptionId}"
|
||||
method : "put"
|
||||
body : requestBody
|
||||
}, (error, response, responseBody) =>
|
||||
return callback(error) if error?
|
||||
@_parseSubscriptionXml responseBody, callback
|
||||
RecurlyWrapper._parseSubscriptionXml responseBody, callback
|
||||
)
|
||||
|
||||
createFixedAmmountCoupon: (coupon_code, name, currencyCode, discount_in_cents, plan_code, callback)->
|
||||
|
@ -191,7 +387,7 @@ module.exports = RecurlyWrapper =
|
|||
</coupon>
|
||||
"""
|
||||
logger.log coupon_code:coupon_code, requestBody:requestBody, "creating coupon"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "coupons"
|
||||
method : "post"
|
||||
body : requestBody
|
||||
|
@ -203,16 +399,16 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
|
||||
lookupCoupon: (coupon_code, callback)->
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "coupons/#{coupon_code}"
|
||||
}, (error, response, body) =>
|
||||
return callback(error) if error?
|
||||
@_parseXml body, callback
|
||||
RecurlyWrapper._parseXml body, callback
|
||||
)
|
||||
|
||||
cancelSubscription: (subscriptionId, callback) ->
|
||||
logger.log subscriptionId:subscriptionId, "telling recurly to cancel subscription"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "subscriptions/#{subscriptionId}/cancel",
|
||||
method: "put"
|
||||
}, (error, response, body) ->
|
||||
|
@ -221,7 +417,7 @@ module.exports = RecurlyWrapper =
|
|||
|
||||
reactivateSubscription: (subscriptionId, callback) ->
|
||||
logger.log subscriptionId:subscriptionId, "telling recurly to reactivating subscription"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url: "subscriptions/#{subscriptionId}/reactivate",
|
||||
method: "put"
|
||||
}, (error, response, body) ->
|
||||
|
@ -237,7 +433,7 @@ module.exports = RecurlyWrapper =
|
|||
</redemption>
|
||||
"""
|
||||
logger.log account_code:account_code, coupon_code:coupon_code, requestBody:requestBody, "redeeming coupon for user"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "coupons/#{coupon_code}/redeem"
|
||||
method : "post"
|
||||
body : requestBody
|
||||
|
@ -251,7 +447,7 @@ module.exports = RecurlyWrapper =
|
|||
next_renewal_date = new Date()
|
||||
next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire)
|
||||
logger.log subscriptionId:subscriptionId, daysUntilExpire:daysUntilExpire, "Exending Free trial for user"
|
||||
@apiRequest({
|
||||
RecurlyWrapper.apiRequest({
|
||||
url : "/subscriptions/#{subscriptionId}/postpone?next_renewal_date=#{next_renewal_date}&bulk=false"
|
||||
method : "put"
|
||||
}, (error, response, responseBody) =>
|
||||
|
@ -261,7 +457,7 @@ module.exports = RecurlyWrapper =
|
|||
)
|
||||
|
||||
_parseSubscriptionXml: (xml, callback) ->
|
||||
@_parseXml xml, (error, data) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.subscription?
|
||||
recurlySubscription = data.subscription
|
||||
|
@ -270,7 +466,7 @@ module.exports = RecurlyWrapper =
|
|||
callback null, recurlySubscription
|
||||
|
||||
_parseAccountXml: (xml, callback) ->
|
||||
@_parseXml xml, (error, data) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.account?
|
||||
account = data.account
|
||||
|
@ -278,6 +474,15 @@ module.exports = RecurlyWrapper =
|
|||
return callback "I don't understand the response from Recurly"
|
||||
callback null, account
|
||||
|
||||
_parseBillingInfoXml: (xml, callback) ->
|
||||
RecurlyWrapper._parseXml xml, (error, data) ->
|
||||
return callback(error) if error?
|
||||
if data? and data.billing_info?
|
||||
billingInfo = data.billing_info
|
||||
else
|
||||
return callback "I don't understand the response from Recurly"
|
||||
callback null, billingInfo
|
||||
|
||||
_parseXml: (xml, callback) ->
|
||||
convertDataTypes = (data) ->
|
||||
if data? and data["$"]?
|
||||
|
@ -315,6 +520,3 @@ module.exports = RecurlyWrapper =
|
|||
return callback(error) if error?
|
||||
result = convertDataTypes(data)
|
||||
callback null, result
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -82,9 +82,9 @@ module.exports = SubscriptionController =
|
|||
logger.log user: user, "redirecting to plans"
|
||||
res.redirect "/user/subscription/plans"
|
||||
else
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groups) ->
|
||||
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, subscription, groupSubscriptions) ->
|
||||
return next(error) if error?
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groups:groups, "showing subscription dashboard"
|
||||
logger.log user: user, subscription:subscription, hasSubOrIsGroupMember:hasSubOrIsGroupMember, groupSubscriptions:groupSubscriptions, "showing subscription dashboard"
|
||||
plans = SubscriptionViewModelBuilder.buildViewModel()
|
||||
res.render "subscriptions/dashboard",
|
||||
title: "your_subscription"
|
||||
|
@ -92,8 +92,9 @@ module.exports = SubscriptionController =
|
|||
taxRate:subscription?.taxRate
|
||||
plans: plans
|
||||
subscription: subscription || {}
|
||||
groups: groups
|
||||
groupSubscriptions: groupSubscriptions
|
||||
subscriptionTabActive: true
|
||||
user:user
|
||||
|
||||
|
||||
userCustomSubscriptionPage: (req, res, next)->
|
||||
|
|
|
@ -8,7 +8,7 @@ _ = require("underscore")
|
|||
|
||||
module.exports =
|
||||
|
||||
buildUsersSubscriptionViewModel: (user, callback = (error, subscription, groups) ->) ->
|
||||
buildUsersSubscriptionViewModel: (user, callback = (error, subscription, memberSubscriptions) ->) ->
|
||||
SubscriptionLocator.getUsersSubscription user, (err, subscription) ->
|
||||
return callback(err) if err?
|
||||
SubscriptionLocator.getMemberSubscriptions user, (err, memberSubscriptions = []) ->
|
||||
|
@ -19,6 +19,7 @@ module.exports =
|
|||
RecurlyWrapper.getSubscription subscription.recurlySubscription_id, (err, recurlySubscription)->
|
||||
tax = recurlySubscription?.tax_in_cents || 0
|
||||
callback null, {
|
||||
admin_id:subscription.admin_id
|
||||
name: plan.name
|
||||
nextPaymentDueAt: SubscriptionFormatters.formatDate(recurlySubscription?.current_period_ends_at)
|
||||
state: recurlySubscription?.state
|
||||
|
|
|
@ -8,6 +8,7 @@ logger = require("logger-sharelatex")
|
|||
metrics = require("../../infrastructure/Metrics")
|
||||
Url = require("url")
|
||||
AuthenticationManager = require("../Authentication/AuthenticationManager")
|
||||
UserSessionsManager = require("./UserSessionsManager")
|
||||
UserUpdater = require("./UserUpdater")
|
||||
settings = require "settings-sharelatex"
|
||||
|
||||
|
@ -72,6 +73,7 @@ module.exports = UserController =
|
|||
if err?
|
||||
logger.err err:err, user_id:user_id, "error getting user for email update"
|
||||
return res.send 500
|
||||
req.session.user.email = user.email
|
||||
UserHandler.populateGroupLicenceInvite user, (err)-> #need to refresh this in the background
|
||||
if err?
|
||||
logger.err err:err, "error populateGroupLicenceInvite"
|
||||
|
@ -80,9 +82,12 @@ module.exports = UserController =
|
|||
logout : (req, res)->
|
||||
metrics.inc "user.logout"
|
||||
logger.log user: req?.session?.user, "logging out"
|
||||
sessionId = req.sessionID
|
||||
user = req?.session?.user
|
||||
req.session.destroy (err)->
|
||||
if err
|
||||
logger.err err: err, 'error destorying session'
|
||||
UserSessionsManager.untrackSession(user, sessionId)
|
||||
res.redirect '/login'
|
||||
|
||||
register : (req, res, next = (error) ->)->
|
||||
|
@ -116,6 +121,8 @@ module.exports = UserController =
|
|||
logger.log user: user, "password changed"
|
||||
AuthenticationManager.setUserPassword user._id, newPassword1, (error) ->
|
||||
return next(error) if error?
|
||||
UserSessionsManager.revokeAllUserSessions user, [req.sessionID], (err) ->
|
||||
return next(err) if err?
|
||||
res.send
|
||||
message:
|
||||
type:'success'
|
||||
|
@ -126,7 +133,3 @@ module.exports = UserController =
|
|||
message:
|
||||
type:'error'
|
||||
text:'Your old password is wrong'
|
||||
|
||||
changeEmailAddress: (req, res)->
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
User = require("../../models/User").User
|
||||
UserLocator = require("./UserLocator")
|
||||
logger = require("logger-sharelatex")
|
||||
|
||||
module.exports =
|
||||
|
||||
|
@ -12,15 +13,22 @@ module.exports =
|
|||
self.createNewUser email:email, holdingAccount:true, callback
|
||||
|
||||
createNewUser: (opts, callback)->
|
||||
logger.log opts:opts, "creating new user"
|
||||
user = new User()
|
||||
user.email = opts.email
|
||||
user.holdingAccount = opts.holdingAccount
|
||||
|
||||
username = opts.email.match(/^[^@]*/)
|
||||
if username?
|
||||
if opts.first_name? and opts.first_name.length != 0
|
||||
user.first_name = opts.first_name
|
||||
else if username?
|
||||
user.first_name = username[0]
|
||||
else
|
||||
user.first_name = ""
|
||||
|
||||
if opts.last_name?
|
||||
user.last_name = opts.last_name
|
||||
else
|
||||
user.last_name = ""
|
||||
|
||||
user.featureSwitches?.pdfng = true
|
||||
|
|
|
@ -6,7 +6,7 @@ logger = require("logger-sharelatex")
|
|||
|
||||
module.exports = UserHandler =
|
||||
|
||||
populateGroupLicenceInvite: (user, callback)->
|
||||
populateGroupLicenceInvite: (user, callback = ->)->
|
||||
logger.log user_id:user._id, "populating any potential group licence invites"
|
||||
licence = SubscriptionDomainHandler.getLicenceUserCanJoin user
|
||||
if !licence?
|
||||
|
|
|
@ -26,6 +26,7 @@ module.exports =
|
|||
activateAccountPage: (req, res) ->
|
||||
# An 'activation' is actually just a password reset on an account that
|
||||
# was set with a random password originally.
|
||||
logger.log query:req.query, "activiate account page called"
|
||||
if !req.query?.user_id? or !req.query?.token?
|
||||
return ErrorController.notFound(req, res)
|
||||
|
||||
|
@ -34,6 +35,7 @@ module.exports =
|
|||
if !user
|
||||
return ErrorController.notFound(req, res)
|
||||
if user.loginCount > 0
|
||||
logger.log user:user, "user has already logged in so is active, sending them to /login"
|
||||
# Already seen this user, so account must be activate
|
||||
# This lets users keep clicking the 'activate' link in their email
|
||||
# as a way to log in which, if I know our users, they will.
|
||||
|
|
|
@ -35,7 +35,8 @@ module.exports = UserRegistrationHandler =
|
|||
|
||||
_createNewUserIfRequired: (user, userDetails, callback)->
|
||||
if !user?
|
||||
UserCreator.createNewUser {holdingAccount:false, email:userDetails.email}, callback
|
||||
userDetails.holdingAccount = false
|
||||
UserCreator.createNewUser {holdingAccount:false, email:userDetails.email, first_name:userDetails.first_name, last_name:userDetails.last_name}, callback
|
||||
else
|
||||
callback null, user
|
||||
|
||||
|
|
126
services/web/app/coffee/Features/User/UserSessionsManager.coffee
Normal file
|
@ -0,0 +1,126 @@
|
|||
Settings = require('settings-sharelatex')
|
||||
redis = require('redis-sharelatex')
|
||||
logger = require("logger-sharelatex")
|
||||
Async = require('async')
|
||||
_ = require('underscore')
|
||||
|
||||
rclient = redis.createClient(Settings.redis.web)
|
||||
|
||||
module.exports = UserSessionsManager =
|
||||
|
||||
_sessionSetKey: (user) ->
|
||||
return "UserSessions:#{user._id}"
|
||||
|
||||
# mimic the key used by the express sessions
|
||||
_sessionKey: (sessionId) ->
|
||||
return "sess:#{sessionId}"
|
||||
|
||||
trackSession: (user, sessionId, callback=(err)-> ) ->
|
||||
if !user?
|
||||
logger.log {sessionId}, "no user to track, returning"
|
||||
return callback(null)
|
||||
if !sessionId?
|
||||
logger.log {user_id: user._id}, "no sessionId to track, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, sessionId}, "onLogin handler"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
value = UserSessionsManager._sessionKey sessionId
|
||||
rclient.multi()
|
||||
.sadd(sessionSetKey, value)
|
||||
.expire(sessionSetKey, "#{Settings.cookieSessionLength}")
|
||||
.exec (err, response) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error while adding session key to UserSessions set"
|
||||
return callback(err)
|
||||
UserSessionsManager._checkSessions(user, () ->)
|
||||
callback()
|
||||
|
||||
untrackSession: (user, sessionId, callback=(err)-> ) ->
|
||||
if !user?
|
||||
logger.log {sessionId}, "no user to untrack, returning"
|
||||
return callback(null)
|
||||
if !sessionId?
|
||||
logger.log {user_id: user._id}, "no sessionId to untrack, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, sessionId}, "onLogout handler"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
value = UserSessionsManager._sessionKey sessionId
|
||||
rclient.multi()
|
||||
.srem(sessionSetKey, value)
|
||||
.expire(sessionSetKey, "#{Settings.cookieSessionLength}")
|
||||
.exec (err, response) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error while removing session key from UserSessions set"
|
||||
return callback(err)
|
||||
UserSessionsManager._checkSessions(user, () ->)
|
||||
callback()
|
||||
|
||||
revokeAllUserSessions: (user, retain, callback=(err)->) ->
|
||||
if !retain
|
||||
retain = []
|
||||
retain = retain.map((i) -> UserSessionsManager._sessionKey(i))
|
||||
if !user
|
||||
logger.log {}, "no user to revoke sessions for, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id}, "revoking all existing sessions for user"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
|
||||
return callback(err)
|
||||
keysToDelete = _.filter(sessionKeys, (k) -> k not in retain)
|
||||
if keysToDelete.length == 0
|
||||
logger.log {user_id: user._id}, "no sessions in UserSessions set to delete, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id, count: keysToDelete.length}, "deleting sessions for user"
|
||||
rclient.multi()
|
||||
.del(keysToDelete)
|
||||
.srem(sessionSetKey, keysToDelete)
|
||||
.exec (err, result) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error revoking all sessions for user"
|
||||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
touch: (user, callback=(err)->) ->
|
||||
if !user
|
||||
logger.log {}, "no user to touch sessions for, returning"
|
||||
return callback(null)
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
rclient.expire sessionSetKey, "#{Settings.cookieSessionLength}", (err, response) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id}, "error while updating ttl on UserSessions set"
|
||||
return callback(err)
|
||||
callback(null)
|
||||
|
||||
_checkSessions: (user, callback=(err)->) ->
|
||||
if !user
|
||||
logger.log {}, "no user, returning"
|
||||
return callback(null)
|
||||
logger.log {user_id: user._id}, "checking sessions for user"
|
||||
sessionSetKey = UserSessionsManager._sessionSetKey(user)
|
||||
rclient.smembers sessionSetKey, (err, sessionKeys) ->
|
||||
if err?
|
||||
logger.err {err, user_id: user._id, sessionSetKey}, "error getting contents of UserSessions set"
|
||||
return callback(err)
|
||||
logger.log {user_id: user._id, count: sessionKeys.length}, "checking sessions for user"
|
||||
Async.series(
|
||||
sessionKeys.map(
|
||||
(key) ->
|
||||
(next) ->
|
||||
rclient.get key, (err, val) ->
|
||||
if err?
|
||||
return next(err)
|
||||
if !val?
|
||||
logger.log {user_id: user._id, key}, ">> removing key from UserSessions set"
|
||||
rclient.srem sessionSetKey, key, (err, result) ->
|
||||
if err?
|
||||
return next(err)
|
||||
return next(null)
|
||||
else
|
||||
next()
|
||||
)
|
||||
, (err, results) ->
|
||||
logger.log {user_id: user._id}, "done checking sessions for user"
|
||||
return callback(err)
|
||||
)
|
|
@ -1,81 +0,0 @@
|
|||
request = require("request")
|
||||
settings = require("settings-sharelatex")
|
||||
logger = require("logger-sharelatex")
|
||||
ErrorController = require "../Errors/ErrorController"
|
||||
_ = require("underscore")
|
||||
AuthenticationController = require("../Authentication/AuthenticationController")
|
||||
async = require("async")
|
||||
other_lngs = ["es"]
|
||||
|
||||
module.exports = WikiController =
|
||||
|
||||
|
||||
_checkIfLoginIsNeeded: (req, res, next)->
|
||||
if settings.apis.wiki.requireLogin
|
||||
AuthenticationController.requireLogin()(req, res, next)
|
||||
else
|
||||
next()
|
||||
|
||||
getPage: (req, res, next) ->
|
||||
WikiController._checkIfLoginIsNeeded req, res, ->
|
||||
|
||||
page = req.url.replace(/^\/learn/, "").replace(/^\//, "")
|
||||
if page == ""
|
||||
page = "Main_Page"
|
||||
|
||||
logger.log page: page, "getting page from wiki"
|
||||
if _.include(other_lngs, req.lng)
|
||||
lngPage = "#{page}_#{req.lng}"
|
||||
else
|
||||
lngPage = page
|
||||
jobs =
|
||||
contents: (cb)->
|
||||
WikiController._getPageContent "Contents", cb
|
||||
pageData: (cb)->
|
||||
WikiController._getPageContent lngPage, cb
|
||||
async.parallel jobs, (error, results)->
|
||||
return next(error) if error?
|
||||
{pageData, contents} = results
|
||||
if pageData.content?.length > 280
|
||||
if _.include(other_lngs, req.lng)
|
||||
pageData.title = pageData.title.slice(0, pageData.title.length - (req.lng.length+1) )
|
||||
WikiController._renderPage(pageData, contents, res)
|
||||
else
|
||||
WikiController._getPageContent page, (error, pageData) ->
|
||||
return next(error) if error?
|
||||
WikiController._renderPage(pageData, contents, res)
|
||||
|
||||
|
||||
|
||||
|
||||
_getPageContent: (page, callback = (error, data = { content: "", title: "" }) ->) ->
|
||||
request {
|
||||
url: "#{settings.apis.wiki.url}/learn-scripts/api.php"
|
||||
qs: {
|
||||
page: decodeURI(page)
|
||||
action: "parse"
|
||||
format: "json"
|
||||
}
|
||||
}, (err, response, data)->
|
||||
return callback(err) if err?
|
||||
try
|
||||
data = JSON.parse(data)
|
||||
catch err
|
||||
logger.err err:err, data:data, "error parsing data from wiki"
|
||||
result =
|
||||
content: data?.parse?.text?['*']
|
||||
title: data?.parse?.title
|
||||
callback null, result
|
||||
|
||||
|
||||
_renderPage: (page, contents, res)->
|
||||
if page.title == "Main Page"
|
||||
title = "Documentation"
|
||||
else
|
||||
title = page.title
|
||||
|
||||
res.render "wiki/page", {
|
||||
page: page
|
||||
contents: contents
|
||||
title: title
|
||||
}
|
|
@ -7,15 +7,19 @@ querystring = require('querystring')
|
|||
SystemMessageManager = require("../Features/SystemMessages/SystemMessageManager")
|
||||
_ = require("underscore")
|
||||
Modules = require "./Modules"
|
||||
Url = require "url"
|
||||
|
||||
fingerprints = {}
|
||||
Path = require 'path'
|
||||
|
||||
|
||||
jsPath =
|
||||
if Settings.useMinifiedJs
|
||||
"/minjs/"
|
||||
else
|
||||
"/js/"
|
||||
|
||||
|
||||
logger.log "Generating file fingerprints..."
|
||||
for path in [
|
||||
"#{jsPath}libs/require.js",
|
||||
|
@ -38,6 +42,17 @@ for path in [
|
|||
else
|
||||
logger.log filePath:filePath, "file does not exist for fingerprints"
|
||||
|
||||
getFingerprint = (path) ->
|
||||
if fingerprints[path]?
|
||||
return fingerprints[path]
|
||||
else
|
||||
logger.err "No fingerprint for file: #{path}"
|
||||
return ""
|
||||
|
||||
logger.log "Finished generating file fingerprints"
|
||||
|
||||
cdnAvailable = Settings.cdn?.web?.host?
|
||||
darkCdnAvailable = Settings.cdn?.web?.darkHost?
|
||||
|
||||
module.exports = (app, webRouter, apiRouter)->
|
||||
webRouter.use (req, res, next)->
|
||||
|
@ -45,9 +60,55 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
|
||||
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
|
||||
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
|
||||
isLive = !isDark and !isSmoke
|
||||
|
||||
if cdnAvailable and isLive
|
||||
staticFilesBase = Settings.cdn?.web?.host
|
||||
else if darkCdnAvailable and isDark
|
||||
staticFilesBase = Settings.cdn?.web?.darkHost
|
||||
else
|
||||
staticFilesBase = ""
|
||||
|
||||
res.locals.jsPath = jsPath
|
||||
res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath)
|
||||
|
||||
|
||||
res.locals.buildJsPath = (jsFile, opts = {})->
|
||||
path = Path.join(jsPath, jsFile)
|
||||
|
||||
doFingerPrint = opts.fingerprint != false
|
||||
|
||||
if !opts.qs?
|
||||
opts.qs = {}
|
||||
|
||||
if !opts.qs?.fingerprint? and doFingerPrint
|
||||
opts.qs.fingerprint = getFingerprint(path)
|
||||
|
||||
if opts.cdn != false
|
||||
path = Url.resolve(staticFilesBase, path)
|
||||
|
||||
qs = querystring.stringify(opts.qs)
|
||||
|
||||
if qs? and qs.length > 0
|
||||
path = path + "?" + qs
|
||||
return path
|
||||
|
||||
|
||||
res.locals.buildCssPath = (cssFile)->
|
||||
path = Path.join("/stylesheets/", cssFile)
|
||||
return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path)
|
||||
|
||||
res.locals.buildImgPath = (imgFile)->
|
||||
path = Path.join("/img/", imgFile)
|
||||
return Url.resolve(staticFilesBase, path)
|
||||
|
||||
next()
|
||||
|
||||
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.settings = Settings
|
||||
next()
|
||||
|
@ -113,12 +174,7 @@ module.exports = (app, webRouter, apiRouter)->
|
|||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
res.locals.fingerprint = (path) ->
|
||||
if fingerprints[path]?
|
||||
return fingerprints[path]
|
||||
else
|
||||
logger.err "No fingerprint for file: #{path}"
|
||||
return ""
|
||||
res.locals.fingerprint = getFingerprint
|
||||
next()
|
||||
|
||||
webRouter.use (req, res, next)->
|
||||
|
|
|
@ -31,6 +31,7 @@ translations = require("translations-sharelatex").setup(Settings.i18n)
|
|||
Modules = require "./Modules"
|
||||
|
||||
ErrorController = require "../Features/Errors/ErrorController"
|
||||
UserSessionsManager = require "../Features/User/UserSessionsManager"
|
||||
|
||||
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)
|
||||
|
@ -89,6 +90,8 @@ webRouter.use translations.setLangBasedOnDomainMiddlewear
|
|||
# Measure expiry from last request, not last login
|
||||
webRouter.use (req, res, next) ->
|
||||
req.session.touch()
|
||||
if req?.session?.user?
|
||||
UserSessionsManager.touch(req.session.user, (err)->)
|
||||
next()
|
||||
|
||||
webRouter.use ReferalConnect.use
|
||||
|
@ -98,19 +101,19 @@ if app.get('env') == 'production'
|
|||
logger.info "Production Enviroment"
|
||||
app.enable('view cache')
|
||||
|
||||
|
||||
|
||||
app.use (req, res, next)->
|
||||
metrics.inc "http-request"
|
||||
crawlerLogger.log(req)
|
||||
next()
|
||||
|
||||
app.use (req, res, next) ->
|
||||
if !Settings.editorIsOpen
|
||||
webRouter.use (req, res, next) ->
|
||||
if Settings.editorIsOpen
|
||||
next()
|
||||
else if req.url.indexOf("/admin") == 0
|
||||
next()
|
||||
else
|
||||
res.status(503)
|
||||
res.render("general/closed", {title:"maintenance"})
|
||||
else
|
||||
next()
|
||||
|
||||
apiRouter.get "/status", (req, res)->
|
||||
res.send("web sharelatex is alive")
|
||||
|
|
|
@ -58,6 +58,7 @@ UserSchema = new Schema
|
|||
mendeley: Boolean # coerce the refProviders values to Booleans
|
||||
zotero: Boolean
|
||||
}
|
||||
betaProgram: { type:Boolean, default: false}
|
||||
|
||||
conn = mongoose.createConnection(Settings.mongo.url, server: poolSize: 10)
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter")
|
|||
StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter")
|
||||
ChatController = require("./Features/Chat/ChatController")
|
||||
BlogController = require("./Features/Blog/BlogController")
|
||||
WikiController = require("./Features/Wiki/WikiController")
|
||||
Modules = require "./infrastructure/Modules"
|
||||
RateLimiterMiddlewear = require('./Features/Security/RateLimiterMiddlewear')
|
||||
RealTimeProxyRouter = require('./Features/RealTimeProxy/RealTimeProxyRouter')
|
||||
|
@ -38,6 +37,7 @@ InactiveProjectController = require("./Features/InactiveData/InactiveProjectCont
|
|||
ContactRouter = require("./Features/Contacts/ContactRouter")
|
||||
ReferencesController = require('./Features/References/ReferencesController')
|
||||
AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear')
|
||||
BetaProgramController = require('./Features/BetaProgram/BetaProgramController')
|
||||
|
||||
logger = require("logger-sharelatex")
|
||||
_ = require("underscore")
|
||||
|
@ -79,6 +79,7 @@ module.exports = class Router
|
|||
webRouter.get '/blog/*', BlogController.getPage
|
||||
|
||||
webRouter.get '/user/activate', UserPagesController.activateAccountPage
|
||||
AuthenticationController.addEndpointToLoginWhitelist '/user/activate'
|
||||
|
||||
webRouter.get '/user/settings', AuthenticationController.requireLogin(), UserPagesController.settingsPage
|
||||
webRouter.post '/user/settings', AuthenticationController.requireLogin(), UserController.updateUserSettings
|
||||
|
@ -104,7 +105,11 @@ module.exports = class Router
|
|||
webRouter.post '/project/:Project_id/settings/admin', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.updateProjectAdminSettings
|
||||
|
||||
webRouter.post '/project/:Project_id/compile', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.compile
|
||||
webRouter.get '/Project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf
|
||||
webRouter.post '/project/:Project_id/compile/stop', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.stopCompile
|
||||
|
||||
# Used by the web download buttons, adds filename header
|
||||
webRouter.get '/project/:Project_id/output/output.pdf', AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.downloadPdf
|
||||
# Used by the pdf viewers
|
||||
webRouter.get /^\/project\/([^\/]*)\/output\/(.*)$/,
|
||||
((req, res, next) ->
|
||||
params =
|
||||
|
@ -118,12 +123,36 @@ module.exports = class Router
|
|||
((req, res, next) ->
|
||||
params =
|
||||
"Project_id": req.params[0]
|
||||
"build": req.params[1]
|
||||
"build_id": req.params[1]
|
||||
"file": req.params[2]
|
||||
req.params = params
|
||||
next()
|
||||
), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
|
||||
|
||||
# direct url access to output files for user but no build, to retrieve files when build fails
|
||||
webRouter.get /^\/project\/([^\/]*)\/user\/([0-9a-f-]+)\/output\/(.*)$/,
|
||||
((req, res, next) ->
|
||||
params =
|
||||
"Project_id": req.params[0]
|
||||
"user_id": req.params[1]
|
||||
"file": req.params[2]
|
||||
req.params = params
|
||||
next()
|
||||
), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
|
||||
|
||||
# direct url access to output files for a specific user and build (query string not required)
|
||||
webRouter.get /^\/project\/([^\/]*)\/user\/([0-9a-f]+)\/build\/([0-9a-f-]+)\/output\/(.*)$/,
|
||||
((req, res, next) ->
|
||||
params =
|
||||
"Project_id": req.params[0]
|
||||
"user_id": req.params[1]
|
||||
"build_id": req.params[2]
|
||||
"file": req.params[3]
|
||||
req.params = params
|
||||
next()
|
||||
), AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.getFileFromClsi
|
||||
|
||||
|
||||
webRouter.delete "/project/:Project_id/output", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.deleteAuxFiles
|
||||
webRouter.get "/project/:Project_id/sync/code", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncCode
|
||||
webRouter.get "/project/:Project_id/sync/pdf", AuthorizationMiddlewear.ensureUserCanReadProject, CompileController.proxySyncPdf
|
||||
|
@ -187,16 +216,13 @@ module.exports = class Router
|
|||
webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages
|
||||
webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage
|
||||
|
||||
webRouter.get /learn(\/.*)?/, RateLimiterMiddlewear.rateLimit({
|
||||
endpointName: "wiki"
|
||||
params: []
|
||||
maxRequests: 60
|
||||
timeInterval: 60
|
||||
}), WikiController.getPage
|
||||
|
||||
webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index
|
||||
webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll
|
||||
|
||||
webRouter.get "/beta/participate", AuthenticationController.requireLogin(), BetaProgramController.optInPage
|
||||
webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
|
||||
webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
|
||||
|
||||
#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
|
||||
|
@ -216,9 +242,11 @@ module.exports = class Router
|
|||
apiRouter.get '/status', (req,res)->
|
||||
res.send("websharelatex is up")
|
||||
|
||||
webRouter.get '/dev/csrf', (req, res) ->
|
||||
res.send res.locals.csrfToken
|
||||
|
||||
webRouter.get '/health_check', HealthCheckController.check
|
||||
webRouter.get '/health_check/redis', HealthCheckController.checkRedis
|
||||
apiRouter.get '/health_check', HealthCheckController.check
|
||||
apiRouter.get '/health_check/redis', HealthCheckController.checkRedis
|
||||
|
||||
apiRouter.get "/status/compiler/:Project_id", AuthorizationMiddlewear.ensureUserCanReadProject, (req, res) ->
|
||||
sendRes = _.once (statusCode, message)->
|
||||
|
|
|
@ -9,6 +9,22 @@ block content
|
|||
.page-header
|
||||
h1 Admin Panel
|
||||
tabset(ng-cloak)
|
||||
tab(heading="System Messages")
|
||||
each message in systemMessages
|
||||
.alert.alert-info.row-spaced !{message.content}
|
||||
hr
|
||||
form(enctype='multipart/form-data', method='post', action='/admin/messages')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
.form-group
|
||||
label(for="content")
|
||||
input.form-control(name="content", type="text", placeholder="Message...", required)
|
||||
button.btn.btn-primary(type="submit") Post Message
|
||||
hr
|
||||
form(enctype='multipart/form-data', method='post', action='/admin/messages/clear')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Clear all messages
|
||||
|
||||
|
||||
tab(heading="Open Sockets")
|
||||
.row-spaced
|
||||
ul
|
||||
|
@ -66,19 +82,6 @@ block content
|
|||
.form-group
|
||||
button.btn-primary.btn(type='submit') Poll
|
||||
|
||||
tab(heading="System Messages")
|
||||
each message in systemMessages
|
||||
.alert.alert-info.row-spaced !{message.content}
|
||||
hr
|
||||
form(enctype='multipart/form-data', method='post', action='/admin/messages')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
.form-group
|
||||
label(for="content")
|
||||
input.form-control(name="content", type="text", placeholder="Message...", required)
|
||||
button.btn.btn-primary(type="submit") Post Message
|
||||
hr
|
||||
form(enctype='multipart/form-data', method='post', action='/admin/messages/clear')
|
||||
input(name="_csrf", type="hidden", value=csrfToken)
|
||||
button.btn.btn-danger(type="submit") Clear all messages
|
||||
|
||||
|
||||
|
||||
|
|
47
services/web/app/views/beta_program/opt_in.jade
Normal file
|
@ -0,0 +1,47 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt
|
||||
.container.beta-opt-in-wrapper
|
||||
.row
|
||||
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
|
||||
.card
|
||||
.page-header.text-centered
|
||||
h1
|
||||
| #{translate("sharelatex_beta_program")}
|
||||
.beta-opt-in
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-12
|
||||
p.text-centered #{translate("beta_program_benefits")}
|
||||
p.text-centered
|
||||
| #{translate("beta_program_badge_description")}
|
||||
span.beta-feature-badge
|
||||
p.text-centered
|
||||
| #{translate("beta_program_current_beta_features_description")}
|
||||
ul.list-unstyled.text-center
|
||||
li
|
||||
i.fa.fa-fw.fa-book
|
||||
| #{translate("mendeley_integration")}
|
||||
.row.text-centered
|
||||
.col-md-12
|
||||
if user.betaProgram
|
||||
p #{translate("beta_program_already_participating")}
|
||||
form(id="beta-program-opt-out", method="post", action="/beta/opt-out", novalidate)
|
||||
.form-group
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
button.btn.btn-primary(
|
||||
type="submit"
|
||||
)
|
||||
span #{translate("beta_program_opt_out_action")}
|
||||
.form-group
|
||||
a(href="/project").btn.btn-info #{translate("back_to_your_projects")}
|
||||
else
|
||||
form(id="beta-program-opt-in", method="post", action="/beta/opt-in", novalidate)
|
||||
.form-group
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
button.btn.btn-primary(
|
||||
type="submit"
|
||||
)
|
||||
span #{translate("beta_program_opt_in_action")}
|
||||
|
|
@ -11,7 +11,19 @@ script(type='text/ng-template', id='supportModalTemplate')
|
|||
label
|
||||
| #{translate("subject")}
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.subject", maxlength='255', tabindex='1', onkeyup='')
|
||||
input.field.text.medium.span8.form-control(
|
||||
ng-model="form.subject",
|
||||
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 350, 'blur': 0} }"
|
||||
maxlength='255',
|
||||
tabindex='1',
|
||||
onkeyup='')
|
||||
.contact-suggestions(ng-show="suggestions.length")
|
||||
p.contact-suggestion-label !{translate("kb_suggestions_enquiry", { kbLink: "<a href='learn/kb' target='_blank'>__kb__</a>", kb: translate("knowledge_base") })}
|
||||
ul.contact-suggestion-list
|
||||
li(ng-repeat="suggestion in suggestions")
|
||||
a.contact-suggestion-list-item(ng-href="{{ suggestion.url }}", ng-click="clickSuggestionLink(suggestion.url);" target="_blank")
|
||||
span(ng-bind-html="suggestion.name")
|
||||
i.fa.fa-angle-right
|
||||
label.desc(ng-show="'#{getUserEmail()}'.length < 1")
|
||||
| #{translate("email")}
|
||||
.form-group(ng-show="'#{getUserEmail()}'.length < 1")
|
||||
|
@ -21,7 +33,7 @@ script(type='text/ng-template', id='supportModalTemplate')
|
|||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.project_url", tabindex='3', onkeyup='')
|
||||
label.desc
|
||||
| #{translate("suggestion")}
|
||||
| #{translate("contact_message_label")}
|
||||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', tabindex='4', onkeyup='')
|
||||
.form-group.text-center
|
||||
|
|
|
@ -3,7 +3,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
head
|
||||
title Something went wrong
|
||||
link(rel="icon", href="/favicon.ico")
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
|
||||
body
|
||||
.content
|
||||
|
@ -12,7 +12,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
.col-md-8.col-md-offset-2.text-center
|
||||
.page-header
|
||||
h2 Oh dear, something went wrong.
|
||||
p: img(src="/img/lion-sad-128.png", alt="Sad Lion")
|
||||
p: img(src=buildImgPath("lion-sad-128.png"), alt="Sad Lion")
|
||||
p
|
||||
| Something went wrong with your request, sorry. Our staff are probably looking into this, but if it continues, please contact us at #{settings.adminEmail}
|
||||
p
|
||||
|
|
|
@ -13,13 +13,12 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
|
||||
|
||||
-if (typeof(title) == "undefined")
|
||||
title ShareLaTeX, the Online LaTeX Editor
|
||||
title= 'ShareLaTeX, '+ translate("online_latex_editor")
|
||||
-else
|
||||
title= translate(title) + ' - ShareLaTeX, '+translate("online_latex_editor")
|
||||
title= translate(title) + ' - ShareLaTeX, ' + translate("online_latex_editor")
|
||||
|
||||
link(rel="icon", href="/favicon.ico")
|
||||
link(rel='stylesheet', href='/stylesheets/style.css?fingerprint='+fingerprint('/stylesheets/style.css'))
|
||||
link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet")
|
||||
link(rel='stylesheet', href=buildCssPath('/style.css'))
|
||||
|
||||
if settings.i18n.subdomainLang
|
||||
each subdomainDetails in settings.i18n.subdomainLang
|
||||
|
@ -30,7 +29,7 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
meta(itemprop="name", content="ShareLaTeX, the Online LaTeX Editor")
|
||||
|
||||
-if (typeof(meta) == "undefined")
|
||||
meta(itemprop="description", name="description", content="An online LaTeX editor that's easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.")
|
||||
meta(itemprop="description", name="description", content='#{translate("site_description")}')
|
||||
-else
|
||||
meta(itemprop="description", name="description" , content=meta)
|
||||
|
||||
|
@ -46,15 +45,84 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
ga('send', 'pageview');
|
||||
- else
|
||||
script(type='text/javascript').
|
||||
window.ga = function() { console.log("Sending to GA", arguments) };
|
||||
window.ga = function() { console.log("would send to GA", arguments) };
|
||||
|
||||
// Countly Analytics
|
||||
if (settings.analytics && settings.analytics.countly && settings.analytics.countly.token)
|
||||
script(type="text/javascript").
|
||||
var Countly = Countly || {};
|
||||
Countly.q = Countly.q || [];
|
||||
Countly.app_key = '#{settings.analytics.countly.token}';
|
||||
Countly.url = '#{settings.analytics.countly.server}';
|
||||
!{ session.user ? 'Countly.device_id = "' + session.user._id + '";' : '' }
|
||||
|
||||
(function() {
|
||||
var cly = document.createElement('script'); cly.type = 'text/javascript';
|
||||
cly.async = true;
|
||||
//enter url of script here
|
||||
cly.src = 'https://cdnjs.cloudflare.com/ajax/libs/countly-sdk-web/16.6.0/countly.min.js';
|
||||
cly.onload = function(){Countly.init()};
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(cly, s);
|
||||
})();
|
||||
|
||||
script(type="text/javascript")
|
||||
if (session && session.user)
|
||||
- var name = session.user.first_name + (session.user.last_name ? ' ' + session.user.last_name : '');
|
||||
| Countly.q.push(['user_details', { email: '#{session.user.email}', name: '#{name}' }]);
|
||||
|
||||
if (justRegistered)
|
||||
| Countly.q.push(['add_event',{ key: 'user-registered' }]);
|
||||
|
||||
if (justLoggedIn)
|
||||
| Countly.q.push(['add_event',{ key: 'user-logged-in' }]);
|
||||
|
||||
if (user && user.features)
|
||||
- featureFlagSet = false;
|
||||
|
||||
if user.features.hasOwnProperty('collaborators')
|
||||
| Countly.q.push([ 'userData.set', 'features-collaborators', #{ user.features.collaborators } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('compileGroup')
|
||||
| Countly.q.push([ 'userData.set', 'features-compileGroup', '#{ user.features.compileGroup }' ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('compileTimeout')
|
||||
| Countly.q.push([ 'userData.set', 'features-compileTimeout', #{ user.features.compileTimeout } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('dropbox')
|
||||
| Countly.q.push([ 'userData.set', 'features-dropbox', #{ user.features.dropbox } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('github')
|
||||
| Countly.q.push([ 'userData.set', 'features-github', #{ user.features.github } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('references')
|
||||
| Countly.q.push([ 'userData.set', 'features-references', #{ user.features.references } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('templates')
|
||||
| Countly.q.push([ 'userData.set', 'features-templates', #{ user.features.templates } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
if user.features.hasOwnProperty('versioning')
|
||||
| Countly.q.push([ 'userData.set', 'features-versioning', #{ user.features.versioning } ]);
|
||||
- featureFlagSet = true;
|
||||
|
||||
|
||||
if featureFlagSet
|
||||
| Countly.q.push(['userData.save'])
|
||||
|
||||
// End countly Analytics
|
||||
|
||||
script(type="text/javascript").
|
||||
window.csrfToken = "#{csrfToken}";
|
||||
|
||||
block scripts
|
||||
script(src="#{jsPath}libs/jquery-1.11.1.min.js")
|
||||
script(src="#{jsPath}libs/angular-1.3.15.min.js")
|
||||
include sentry
|
||||
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
|
||||
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
|
||||
script.
|
||||
window.sharelatex = {
|
||||
siteUrl: '#{settings.siteUrl}',
|
||||
|
@ -123,12 +191,14 @@ html(itemscope, itemtype='http://schema.org/Product')
|
|||
}
|
||||
};
|
||||
script(
|
||||
data-main=jsPath+'main.js',
|
||||
baseurl=jsPath,
|
||||
src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
|
||||
data-main=buildJsPath('main.js', {fingerprint:false}),
|
||||
baseurl=fullJsPath,
|
||||
src=buildJsPath('libs/require.js')
|
||||
)
|
||||
|
||||
|
||||
include contact-us-modal
|
||||
include sentry
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ footer.site-footer
|
|||
aria-expanded="false",
|
||||
tooltip="#{translate('language')}"
|
||||
)
|
||||
img(src="/img/flags/24/#{currentLngCode}.png")
|
||||
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{currentLngCode}")
|
||||
|
||||
ul.dropdown-menu(role="menu")
|
||||
li.dropdown-header #{translate("language")}
|
||||
|
@ -23,9 +23,9 @@ footer.site-footer
|
|||
if !subdomainDetails.hide
|
||||
li.lngOption
|
||||
a.menu-indent(href=subdomainDetails.url+currentUrl)
|
||||
img(src="/img/flags/24/#{subdomainDetails.lngCode}.png")
|
||||
figure(class="sprite-icon sprite-icon-lang sprite-icon-#{subdomainDetails.lngCode}")
|
||||
| #{translate(subdomainDetails.lngCode)}
|
||||
|
||||
//- img(src="/img/flags/24/.png")
|
||||
each item in nav.left_footer
|
||||
li
|
||||
if item.url
|
||||
|
@ -41,4 +41,3 @@ footer.site-footer
|
|||
a(href=item.url, class=item.class) !{item.text}
|
||||
else
|
||||
| !{item.text}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ nav.navbar.navbar-default
|
|||
.navbar-header
|
||||
button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}")
|
||||
i.fa.fa-bars
|
||||
if nav.title
|
||||
if settings.nav.custom_logo
|
||||
a(href='/', style='background-image:url("#{settings.nav.custom_logo}")').navbar-brand
|
||||
else if (nav.title)
|
||||
a(href='/').navbar-title #{nav.title}
|
||||
else
|
||||
a(href='/').navbar-brand
|
||||
|
@ -17,6 +19,8 @@ nav.navbar.navbar-default
|
|||
| Admin
|
||||
b.caret
|
||||
ul.dropdown-menu
|
||||
li
|
||||
a(href="/admin") Manage Site
|
||||
li
|
||||
a(href="/admin/user") Manage Users
|
||||
|
||||
|
|
|
@ -97,9 +97,10 @@ block content
|
|||
window.csrfToken = "!{csrfToken}";
|
||||
window.anonymous = #{anonymous};
|
||||
window.maxDocLength = #{maxDocLength};
|
||||
window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)};
|
||||
window.requirejs = {
|
||||
"paths" : {
|
||||
"mathjax": "/js/libs/mathjax/MathJax.js?config=TeX-AMS_HTML",
|
||||
"mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {qs:{config:'TeX-AMS_HTML', fingerprint:false}})}",
|
||||
"moment": "libs/moment-2.7.0",
|
||||
"libs/pdf": "libs/pdfjs-1.3.91/pdf"
|
||||
},
|
||||
|
@ -128,15 +129,18 @@ block content
|
|||
|
||||
- var pdfPath = 'libs/pdfjs-1.3.91/pdf.worker.js'
|
||||
- var fingerprintedPath = fingerprint(jsPath+pdfPath)
|
||||
- var pdfJsWorkerPath = jsPath+pdfPath+'?fingerprint='+fingerprintedPath
|
||||
- var pdfJsWorkerPath = buildJsPath(pdfPath, {cdn:false,qs:{fingerprint:fingerprintedPath}}) // don't use worker for cdn
|
||||
|
||||
|
||||
script(type='text/javascript').
|
||||
window.pdfJsWorkerPath = "#{pdfJsWorkerPath}";
|
||||
|
||||
script(
|
||||
data-main=jsPath+"ide.js",
|
||||
baseurl=jsPath,
|
||||
data-ace-base=jsPath+'ace',
|
||||
src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js')
|
||||
data-main=buildJsPath("ide.js", {fingerprint:false}),
|
||||
baseurl=fullJsPath,
|
||||
data-ace-base=buildJsPath('ace', {fingerprint:false}),
|
||||
src=buildJsPath('libs/require.js')
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ div.full-size(
|
|||
resize-on="layout:main:resize,layout:pdf:resize",
|
||||
annotations="pdf.logEntryAnnotations[editor.open_doc_id]",
|
||||
read-only="!permissions.write",
|
||||
on-ctrl-enter="recompile"
|
||||
on-ctrl-enter="recompileViaKey"
|
||||
)
|
||||
|
||||
.ui-layout-east
|
||||
|
|
|
@ -15,7 +15,7 @@ aside#left-menu.full-size(
|
|||
| #{translate("source")}
|
||||
li
|
||||
a(
|
||||
ng-href="{{pdf.url}}"
|
||||
ng-href="{{pdf.downloadUrl || pdf.url}}"
|
||||
target="_blank"
|
||||
ng-if="pdf.url"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
div.full-size.pdf(ng-controller="PdfController")
|
||||
.toolbar.toolbar-tall
|
||||
.btn-group(dropdown)
|
||||
.btn-group(
|
||||
dropdown,
|
||||
tooltip-html="'#{translate('recompile_pdf')} <span class=\"keyboard-shortcut\">({{modifierKey}} + Enter)</span>'"
|
||||
tooltip-class="keyboard-tooltip"
|
||||
tooltip-popup-delay="500"
|
||||
tooltip-append-to-body="true"
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
a.btn.btn-info(
|
||||
href,
|
||||
ng-disabled="pdf.compiling",
|
||||
|
@ -29,6 +36,14 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
i.fa.fa-fw(ng-class="{'fa-check': draft}")
|
||||
| #{translate("fast")}
|
||||
span.subdued [draft]
|
||||
a(
|
||||
href
|
||||
ng-click="stop()"
|
||||
ng-show="pdf.compiling",
|
||||
tooltip="#{translate('stop_compile')}"
|
||||
tooltip-placement="bottom"
|
||||
)
|
||||
i.fa.fa-stop()
|
||||
a.log-btn(
|
||||
href
|
||||
ng-click="toggleLogs()"
|
||||
|
@ -46,7 +61,7 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }}
|
||||
|
||||
a(
|
||||
ng-href="{{pdf.url}}"
|
||||
ng-href="{{pdf.downloadUrl || pdf.url}}"
|
||||
target="_blank"
|
||||
ng-if="pdf.url"
|
||||
tooltip="#{translate('download_pdf')}"
|
||||
|
@ -93,15 +108,98 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
'alert-info': entry.level == 'typesetting'\
|
||||
}"
|
||||
ng-click="openInEditor(entry)"
|
||||
ng-init="feedbackSent = false; showNegFeedbackUI = false; negFeedbackReason = ''; negFeedbackReasonFreeText = ''"
|
||||
)
|
||||
span.line-no
|
||||
i.fa.fa-link(aria-hidden="true")
|
||||
|
|
||||
span(ng-show="entry.file") {{ entry.file }}
|
||||
span(ng-show="entry.line") , line {{ entry.line }}
|
||||
p.entry-message(ng-show="entry.message") {{ entry.message }}
|
||||
p.entry-content(ng-show="entry.content") {{ entry.content }}
|
||||
.card.card-hint(
|
||||
ng-if="entry.humanReadableHint"
|
||||
stop-propagation="click"
|
||||
)
|
||||
figure.card-hint-icon-container
|
||||
i.fa.fa-lightbulb-o(aria-hidden="true")
|
||||
p.card-hint-text(
|
||||
ng-show="entry.humanReadableHint",
|
||||
ng-bind-html="wikiEnabled ? entry.humanReadableHint : stripHTMLFromString(entry.humanReadableHint)")
|
||||
.card-hint-actions.clearfix
|
||||
.card-hint-ext-link(ng-if="wikiEnabled")
|
||||
a(
|
||||
ng-href="{{ entry.extraInfoURL }}",
|
||||
ng-click="trackLogHintsLearnMore()"
|
||||
target="_blank"
|
||||
)
|
||||
i.fa.fa-external-link
|
||||
| #{translate("log_hint_extra_info")}
|
||||
.card-hint-feedback(
|
||||
ng-hide="feedbackSent || showNegFeedbackUI"
|
||||
ng-class="entry.ruleId"
|
||||
)
|
||||
label.card-hint-feedback-label #{translate("log_hint_feedback_label")}
|
||||
a.card-hint-feedback-positive(
|
||||
ng-click="trackLogHintsPositiveFeedback(entry.ruleId); feedbackSent = true;"
|
||||
href
|
||||
) #{translate("answer_yes")}
|
||||
span /
|
||||
a.card-hint-feedback-negative(
|
||||
ng-click="trackLogHintsNegativeFeedback(entry.ruleId); showNegFeedbackUI = true;"
|
||||
href
|
||||
) #{translate("answer_no")}
|
||||
.card-hint-extra-feedback(ng-hide="!showNegFeedbackUI || feedbackSent")
|
||||
p.card-hint-extra-feedback-label #{translate("log_hint_ask_extra_feedback")}
|
||||
.radio: label
|
||||
input(
|
||||
type="radio"
|
||||
name="{{ 'neg-feedback-reason-' + $index }}"
|
||||
ng-model="negFeedbackReason"
|
||||
value="{{ logHintsNegFeedbackValues.DIDNT_UNDERSTAND }}"
|
||||
)
|
||||
| #{translate("log_hint_extra_feedback_didnt_understand")}
|
||||
.radio: label
|
||||
input(
|
||||
type="radio"
|
||||
name="{{ 'neg-feedback-reason-' + $index }}"
|
||||
ng-model="negFeedbackReason"
|
||||
value="{{ logHintsNegFeedbackValues.NOT_APPLICABLE }}"
|
||||
)
|
||||
| #{translate("log_hint_extra_feedback_not_applicable")}
|
||||
.radio: label
|
||||
input(
|
||||
type="radio"
|
||||
name="{{ 'neg-feedback-reason-' + $index }}"
|
||||
ng-model="negFeedbackReason"
|
||||
value="{{ logHintsNegFeedbackValues.INCORRECT }}"
|
||||
)
|
||||
| #{translate("log_hint_extra_feedback_incorrect")}
|
||||
.radio: label
|
||||
input(
|
||||
type="radio"
|
||||
name="{{ 'neg-feedback-reason-' + $index }}"
|
||||
ng-model="negFeedbackReason"
|
||||
value="{{ logHintsNegFeedbackValues.OTHER }}"
|
||||
)
|
||||
| #{translate("log_hint_extra_feedback_other")}
|
||||
textarea.form-control(
|
||||
ng-show="negFeedbackReason === logHintsNegFeedbackValues.OTHER"
|
||||
ng-model="negFeedbackReasonFreeText"
|
||||
rows="2"
|
||||
)
|
||||
.clearfix
|
||||
button.btn.btn-default.btn-sm.pull-right(
|
||||
ng-disabled="!negFeedbackReason"
|
||||
ng-click="trackLogHintsNegFeedbackDetails(entry.ruleId, negFeedbackReason, negFeedbackReasonFreeText); feedbackSent = true;"
|
||||
) #{translate("log_hint_extra_feedback_submit")}
|
||||
|
||||
.card-hint-feedback(ng-show="feedbackSent")
|
||||
label.card-hint-feedback-label #{translate("log_hint_feedback_gratitude")}
|
||||
|
||||
p.entry-content(ng-show="entry.content") {{ entry.content.trim() }}
|
||||
|
||||
p
|
||||
.pull-right
|
||||
.files-dropdown-container
|
||||
a.btn.btn-default.btn-sm(
|
||||
href,
|
||||
tooltip="#{translate('clear_cached_files')}",
|
||||
|
@ -111,7 +209,10 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
)
|
||||
i.fa.fa-trash-o
|
||||
|
|
||||
div.dropdown(style="display: inline-block;", dropdown)
|
||||
div.files-dropdown(
|
||||
ng-class="shouldDropUp ? 'dropup' : 'dropdown'"
|
||||
dropdown
|
||||
)
|
||||
a.btn.btn-default.btn-sm(
|
||||
href
|
||||
dropdown-toggle
|
||||
|
@ -154,6 +255,27 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
ng-if="settings.pdfViewer == 'native'"
|
||||
)
|
||||
|
||||
.pdf-validation-problems(ng-switch-when="validation-problems")
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.duplicatePaths")
|
||||
strong #{translate("latex_error")}
|
||||
span #{translate("duplicate_paths_found")}
|
||||
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.sizeCheck")
|
||||
strong #{translate("project_too_large")}
|
||||
div #{translate("project_too_large_please_reduce")}
|
||||
div
|
||||
li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.validation.conflictedPaths")
|
||||
div
|
||||
strong #{translate("conflicting_paths_found")}
|
||||
div #{translate("following_paths_conflict")}
|
||||
div
|
||||
li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }}
|
||||
|
||||
|
||||
.pdf-errors(ng-switch-when="errors")
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.error")
|
||||
|
@ -172,6 +294,10 @@ div.full-size.pdf(ng-controller="PdfController")
|
|||
strong #{translate("server_error")}
|
||||
span #{translate("too_recently_compiled")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.compileTerminated")
|
||||
strong #{translate("terminated")}.
|
||||
span #{translate("compile_terminated_by_user")}
|
||||
|
||||
.alert.alert-danger(ng-show="pdf.timedout")
|
||||
p
|
||||
strong #{translate("timedout")}.
|
||||
|
|
|
@ -49,7 +49,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
|
|||
.form-group
|
||||
tags-input(
|
||||
template="shareTagTemplate"
|
||||
placeholder="joe@example.com, sue@example.com, ..."
|
||||
placeholder="#{settings.customisation.shareProjectPlaceholder || 'joe@example.com, sue@example.com, ...'}"
|
||||
ng-model="inputs.contacts"
|
||||
focus-on="open"
|
||||
display-property="display"
|
||||
|
|
|
@ -94,18 +94,7 @@
|
|||
- if (showUserDetailsArea)
|
||||
span(ng-controller="LeftHandMenuPromoController", ng-cloak)
|
||||
|
||||
.row-spaced(ng-if="showDatajoy")
|
||||
hr
|
||||
.card.card-thin
|
||||
p.text-center.small
|
||||
| <strong>Python</strong> or <strong>R</strong> user?
|
||||
p.text-center.small
|
||||
a(href="https://www.getdatajoy.com/", target="_blank").btn.btn-info.btn-small Try DataJoy
|
||||
p.text-center.small(style="font-size: 0.8em")
|
||||
a(href="https://www.getdatajoy.com/", target="_blank") DataJoy
|
||||
| is a new online Python and R editor from ShareLaTeX.
|
||||
|
||||
.row-spaced#userProfileInformation(ng-if="hasProjects && !showDatajoy")
|
||||
.row-spaced#userProfileInformation(ng-if="hasProjects")
|
||||
div(ng-controller="UserProfileController")
|
||||
hr(ng-show="percentComplete < 100")
|
||||
.text-centered.user-profile(ng-show="percentComplete < 100")
|
||||
|
@ -120,7 +109,7 @@
|
|||
) #{translate("complete")}
|
||||
|
||||
|
||||
.row-spaced(ng-if="hasProjects && userHasSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
|
||||
.row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak, sixpack-switch="left-menu-upgraed-rotation").text-centered
|
||||
span(sixpack-default).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
|
@ -146,7 +135,7 @@
|
|||
p
|
||||
span Get Dropbox Sync
|
||||
p
|
||||
img(src="/img/dropbox/simple_logo.png")
|
||||
img(src=buildImgPath("dropbox/simple_logo.png"))
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
|
@ -159,13 +148,13 @@
|
|||
p
|
||||
span Get Github Sync
|
||||
p
|
||||
img(src="/img/github/octocat.jpg")
|
||||
img(src=buildImgPath("github/octocat.jpg"))
|
||||
p
|
||||
a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus") #{translate("sharing_sl")} .
|
||||
script.
|
||||
window.userHasSubscription = #{settings.enableSubscriptions && !hasSubscription}
|
||||
window.userHasNoSubscription = #{settings.enableSubscriptions && !hasSubscription}
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- if (sentrySrc.match(/^([a-z]+:)?\/\//i))
|
||||
script(src="#{sentrySrc}")
|
||||
- else
|
||||
script(src="#{jsPath}libs/#{sentrySrc}")
|
||||
script(src=buildJsPath("libs/#{sentrySrc}", {fingerprint:false}))
|
||||
- if (typeof(sentrySrc) != "undefined")
|
||||
script(type="text/javascript").
|
||||
if (typeof(Raven) != "undefined" && Raven.config) {
|
||||
|
|
|
@ -43,12 +43,8 @@ block content
|
|||
.card(ng-if="view == 'overview'")
|
||||
.page-header
|
||||
h1 #{translate("your_subscription")}
|
||||
-if (groups.length != 0)
|
||||
each groupSubscription in groups
|
||||
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
||||
span
|
||||
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
|
||||
- else if (subscription)
|
||||
|
||||
- if (subscription && user._id+'' == subscription.admin_id+'')
|
||||
case subscription.state
|
||||
when "free-trial"
|
||||
p !{translate("on_free_trial_expiring_at", {expiresAt:"<strong>" + subscription.expiresAt + "</strong>"})}
|
||||
|
@ -72,15 +68,9 @@ block content
|
|||
p !{translate("your_subscription_has_expired")}
|
||||
a(href="/user/subscription/plans") !{translate("create_new_subscription")}
|
||||
default
|
||||
-if(groups.length == 0)
|
||||
-if(groupSubscriptions.length == 0)
|
||||
p !{translate("problem_with_subscription_contact_us")}
|
||||
|
||||
-if(subscription.groupPlan)
|
||||
a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
|
||||
|
||||
|
||||
|
||||
|
||||
div(ng-show="changePlan", ng-cloak)#changePlanSection
|
||||
h2.col-md-7 !{translate("change_plan")}
|
||||
span.dropdown.col-md-1.changePlanButton(ng-controller="CurrenyDropdownController", style="display:none", dropdown)
|
||||
|
@ -105,6 +95,20 @@ block content
|
|||
mixin printPlans(plans.individualMonthlyPlans)
|
||||
mixin printPlans(plans.individualAnnualPlans)
|
||||
|
||||
|
||||
each groupSubscription in groupSubscriptions
|
||||
- if (user._id+'' != groupSubscription.admin_id._id+'')
|
||||
div
|
||||
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
|
||||
span
|
||||
button.btn.btn-danger(ng-click="removeSelfFromGroup('#{groupSubscription.admin_id._id}')") #{translate("leave_group")}
|
||||
|
||||
-if(subscription.groupPlan && user._id+'' == subscription.admin_id+'')
|
||||
div
|
||||
a(href="/subscription/group").btn.btn-primary !{translate("manage_group")}
|
||||
|
||||
|
||||
|
||||
.card(ng-if="view == 'cancelation'")
|
||||
.page-header
|
||||
h1 #{translate("Cancel Subscription")}
|
||||
|
|
|
@ -3,7 +3,7 @@ extends ../layout
|
|||
block content
|
||||
- locals.supressDefaultJs = true
|
||||
script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath)
|
||||
script(src=jsPath+'libs/recurly.min.js')
|
||||
script(src=buildJsPath('libs/recurly.min.js'))
|
||||
|
||||
.content.content-alt
|
||||
.container
|
||||
|
|
|
@ -30,10 +30,10 @@ block content
|
|||
| Henry and James
|
||||
.portraits
|
||||
span.img-circle
|
||||
img(src="/img/about/henry_oswald.jpg")
|
||||
img(src=buildImgPath("about/henry_oswald.jpg"))
|
||||
|
|
||||
span.img-circle
|
||||
img(src="/img/about/james_allen.jpg")
|
||||
img(src=buildImgPath("about/james_allen.jpg"))
|
||||
p
|
||||
a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
span(ng-controller="TranslationsPopupController", ng-cloak)
|
||||
.translations-message(ng-hide="hidei18nNotification")
|
||||
a(href=recomendSubdomain.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName:"<strong>" + translate(recomendSubdomain.lngCode) + "</strong>"})}
|
||||
img(src="/img/flags/24/#{recomendSubdomain.lngCode}.png")
|
||||
img(src=buildImgPath("flags/24/#{recomendSubdomain.lngCode}.png"))
|
||||
button(ng-click="dismiss()").close.pull-right
|
||||
span(aria-hidden="true") ×
|
||||
span.sr-only #{translate("close")}
|
19
services/web/app/views/university/case_study.jade
Normal file
|
@ -0,0 +1,19 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.masthead
|
||||
.container
|
||||
.row
|
||||
.col-md-12
|
||||
h1 !{viewData.title}
|
||||
.row.row-spaced
|
||||
|
||||
.pattern-container
|
||||
.content
|
||||
.container
|
||||
.row
|
||||
.col-md-10.col-md-offset-1
|
||||
.card
|
||||
.row
|
||||
| !{viewData.html}
|
||||
|
|
@ -99,6 +99,17 @@ block content
|
|||
|
||||
| !{moduleIncludes("userSettings", locals)}
|
||||
|
||||
if (user.betaProgram)
|
||||
hr
|
||||
|
||||
h3
|
||||
| #{translate("sharelatex_beta_program")}
|
||||
|
||||
p.small
|
||||
| #{translate("beta_program_already_participating")}
|
||||
div
|
||||
a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
|
||||
|
||||
hr
|
||||
|
||||
if !externalAuthenticationSystemUsed()
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
extends ../layout
|
||||
|
||||
block content
|
||||
.content.content-alt(ng-cloak)
|
||||
.container.wiki
|
||||
.row.template-page-header
|
||||
.col-md-8(ng-cloak)
|
||||
|
||||
.row
|
||||
.col-xs-3.contents(ng-non-bindable)
|
||||
| !{contents.content}
|
||||
|
||||
.col-xs-9.page
|
||||
- if(typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
|
||||
span(ng-controller="SearchWikiController")
|
||||
.row
|
||||
form.project-search.form-horizontal.col-md-9(role="form")
|
||||
.form-group.has-feedback.has-feedback-left.col-md-12
|
||||
input.form-control.col-md-12(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library....")
|
||||
i.fa.fa-search.form-control-feedback-left
|
||||
i.fa.fa-times.form-control-feedback(
|
||||
ng-click="clearSearchText()",
|
||||
style="cursor: pointer;",
|
||||
ng-show="searchQueryText.length > 0"
|
||||
)
|
||||
.col-md-3.text-right
|
||||
a.btn.btn-primary(ng-click="showMissingTemplateModal()") #{translate("suggest_new_doc")}
|
||||
|
||||
.row
|
||||
.col-md-12(ng-cloak)
|
||||
a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin
|
||||
span(ng-bind-html='hit.name')
|
||||
div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content')
|
||||
|
||||
.card.row-spaced(ng-non-bindable)
|
||||
.page-header
|
||||
h1 #{title}
|
||||
|
||||
| !{page.content}
|
||||
|
||||
|
||||
|
||||
|
||||
script(type="text/ng-template", id="missingWikiPageModal")
|
||||
.modal-header
|
||||
button.close(
|
||||
type="button"
|
||||
data-dismiss="modal"
|
||||
ng-click="close()"
|
||||
) ×
|
||||
h3 #{translate("suggest_new_doc")}
|
||||
.modal-body.contact-us-modal
|
||||
span(ng-show="sent == false")
|
||||
label.desc
|
||||
| #{translate("email")} (#{translate("optional")})
|
||||
.form-group
|
||||
input.field.text.medium.span8.form-control(ng-model="form.email", ng-init="form.email = '#{getUserEmail()}'", type='email', spellcheck='false', value='', maxlength='255', tabindex='2')
|
||||
label.desc
|
||||
| #{translate("suggestion")}
|
||||
.form-group
|
||||
textarea.field.text.medium.span8.form-control(ng-model="form.message",type='text', value='', maxlength='255', tabindex='4', onkeyup='')
|
||||
span(ng-show="sent")
|
||||
p #{translate("request_sent_thank_you")}
|
||||
.modal-footer
|
||||
button.btn.btn-default(ng-click="close()")
|
||||
span #{translate("dismiss")}
|
||||
button.btn-success.btn(type='submit', ng-disabled="sending", ng-click="contactUs()") #{translate("contact_us")}
|
||||
|
|
@ -15,7 +15,7 @@ httpAuthUsers[httpAuthUser] = httpAuthPass
|
|||
|
||||
sessionSecret = "secret-please-change"
|
||||
|
||||
module.exports =
|
||||
module.exports = settings =
|
||||
# File storage
|
||||
# ------------
|
||||
#
|
||||
|
@ -104,16 +104,21 @@ module.exports =
|
|||
url: "http://localhost:3036"
|
||||
sixpack:
|
||||
url: ""
|
||||
references:
|
||||
url: "http://localhost:3040"
|
||||
notifications:
|
||||
url: "http://localhost:3042"
|
||||
# references:
|
||||
# url: "http://localhost:3040"
|
||||
# notifications:
|
||||
# url: "http://localhost:3042"
|
||||
|
||||
templates:
|
||||
user_id: process.env.TEMPLATES_USER_ID or "5395eb7aad1f29a88756c7f2"
|
||||
showSocialButtons: false
|
||||
showComments: false
|
||||
|
||||
# cdn:
|
||||
# web:
|
||||
# host:"http://cdn.sharelatex.dev:3000"
|
||||
# darkHost:"http://cdn.sharelatex.dev:3000"
|
||||
|
||||
# Where your instance of ShareLaTeX can be found publically. Used in emails
|
||||
# that are sent out, generated links, etc.
|
||||
siteUrl : siteUrl = 'http://localhost:3000'
|
||||
|
@ -137,6 +142,7 @@ module.exports =
|
|||
# --------
|
||||
security:
|
||||
sessionSecret: sessionSecret
|
||||
bcryptRounds: 12 # number of rounds used to hash user passwords (raised to power 2)
|
||||
|
||||
httpAuthUsers: httpAuthUsers
|
||||
|
||||
|
@ -262,6 +268,10 @@ module.exports =
|
|||
# public projects, /learn, /templates, about pages, etc.
|
||||
allowPublicAccess: if process.env["SHARELATEX_ALLOW_PUBLIC_ACCESS"] == 'true' then true else false
|
||||
|
||||
# Use a single compile directory for all users in a project
|
||||
# (otherwise each user has their own directory)
|
||||
# disablePerUserCompiles: true
|
||||
|
||||
# Maximum size of text documents in the real-time editing system.
|
||||
max_doc_length: 2 * 1024 * 1024 # 2mb
|
||||
|
||||
|
@ -335,6 +345,8 @@ module.exports =
|
|||
}]
|
||||
}]
|
||||
|
||||
customisation: {}
|
||||
|
||||
# templates: [{
|
||||
# name : "cv_or_resume",
|
||||
# url : "/templates/cv"
|
||||
|
|
3853
services/web/npm-shrinkwrap.json
generated
Normal file
|
@ -18,6 +18,7 @@
|
|||
"body-parser": "^1.13.1",
|
||||
"bufferedstream": "1.6.0",
|
||||
"connect-redis": "2.3.0",
|
||||
"contentful": "^3.3.14",
|
||||
"cookie": "^0.2.3",
|
||||
"cookie-parser": "1.3.5",
|
||||
"csurf": "^1.8.3",
|
||||
|
@ -30,8 +31,9 @@
|
|||
"jade": "~1.3.1",
|
||||
"ldapjs": "^0.7.1",
|
||||
"logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master",
|
||||
"lodash": "^4.13.1",
|
||||
"lynx": "0.1.1",
|
||||
"marked": "^0.3.3",
|
||||
"marked": "^0.3.5",
|
||||
"method-override": "^2.3.3",
|
||||
"metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#v1.3.0",
|
||||
"mimelib": "0.2.14",
|
||||
|
@ -54,32 +56,35 @@
|
|||
"settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#v1.0.0",
|
||||
"sixpack-client": "^1.0.0",
|
||||
"temp": "^0.8.3",
|
||||
"translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
|
||||
"underscore": "1.6.0",
|
||||
"underscore.string": "^3.0.2",
|
||||
"v8-profiler": "^5.2.3",
|
||||
"xml2js": "0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bunyan": "0.22.1",
|
||||
"translations-sharelatex": "git+https://github.com/sharelatex/translations-sharelatex.git#master",
|
||||
"chai": "",
|
||||
"chai-spies": "",
|
||||
"grunt": "0.4.1",
|
||||
"grunt-available-tasks": "0.4.1",
|
||||
"grunt-bunyan": "0.5.0",
|
||||
"grunt-concurrent": "0.4.3",
|
||||
"grunt-contrib-clean": "0.5.0",
|
||||
"grunt-contrib-coffee": "0.10.0",
|
||||
"grunt-contrib-less": "0.9.0",
|
||||
"grunt-contrib-requirejs": "0.4.1",
|
||||
"grunt-execute": "0.1.5",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-env": "0.4.4",
|
||||
"clean-css": "^3.4.18",
|
||||
"grunt-exec": "^0.4.7",
|
||||
"grunt-execute": "^0.2.2",
|
||||
"grunt-file-append": "0.0.6",
|
||||
"grunt-git-rev-parse": "^0.1.4",
|
||||
"grunt-mocha-test": "0.9.0",
|
||||
"grunt-newer": "^1.2.0",
|
||||
"grunt-parallel": "^0.5.1",
|
||||
"grunt-sed": "^0.1.1",
|
||||
"sandboxed-module": "0.2.0",
|
||||
"sinon": "",
|
||||
"timekeeper": "",
|
||||
"grunt-env": "0.4.4"
|
||||
"timekeeper": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ define [
|
|||
ReferencesManager
|
||||
) ->
|
||||
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage) ->
|
||||
App.controller "IdeController", ($scope, $timeout, ide, localStorage, event_tracking) ->
|
||||
# Don't freak out if we're already in an apply callback
|
||||
$scope.$originalApply = $scope.$apply
|
||||
$scope.$apply = (fn = () ->) ->
|
||||
|
@ -69,6 +69,17 @@ define [
|
|||
|
||||
$scope.chat = {}
|
||||
|
||||
# Tracking code.
|
||||
$scope.$watch "ui.view", (newView, oldView) ->
|
||||
if newView? and newView != "editor" and newView != "pdf"
|
||||
event_tracking.sendCountlyOnce "ide-open-view-#{ newView }-once"
|
||||
|
||||
$scope.$watch "ui.chatOpen", (isOpen) ->
|
||||
event_tracking.sendCountlyOnce "ide-open-chat-once" if isOpen
|
||||
|
||||
$scope.$watch "ui.leftMenuShown", (isOpen) ->
|
||||
event_tracking.sendCountlyOnce "ide-open-left-menu-once" if isOpen
|
||||
# End of tracking code.
|
||||
|
||||
window._ide = ide
|
||||
|
||||
|
|
|
@ -74,15 +74,17 @@ define [
|
|||
editor.commands.removeCommand "foldall"
|
||||
|
||||
# For European keyboards, the / is above 7 so needs Shift pressing.
|
||||
# This comes through as Ctrl-Shift-/ which is mapped to toggleBlockComment.
|
||||
# This comes through as Command-Shift-/ on OS X, which is mapped to
|
||||
# toggleBlockComment.
|
||||
# This doesn't do anything for LaTeX, so remap this to togglecomment to
|
||||
# work for European keyboards as normal.
|
||||
# On Windows, the key combo comes as Ctrl-Shift-7.
|
||||
editor.commands.removeCommand "toggleBlockComment"
|
||||
editor.commands.removeCommand "togglecomment"
|
||||
|
||||
editor.commands.addCommand {
|
||||
name: "togglecomment",
|
||||
bindKey: { win: "Ctrl-/|Ctrl-Shift-/", mac: "Command-/|Command-Shift-/" },
|
||||
bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
|
||||
exec: (editor) -> editor.toggleCommentLines(),
|
||||
multiSelectAction: "forEachLine",
|
||||
scrollIntoView: "selectionPart"
|
||||
|
|
|
@ -134,6 +134,8 @@ define [
|
|||
editor.completer.showPopup(editor)
|
||||
editor.completer.cancelContextMenu()
|
||||
$(editor.completer.popup?.container).css({'font-size': @$scope.fontSize + 'px'})
|
||||
if editor.completer?.completions?.filtered?.length == 0
|
||||
editor.completer.detach()
|
||||
bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,10 @@ define [
|
|||
"base"
|
||||
"ace/ace"
|
||||
], (App) ->
|
||||
App.controller "HotkeysController", ($scope, $modal) ->
|
||||
App.controller "HotkeysController", ($scope, $modal, event_tracking) ->
|
||||
$scope.openHotkeysModal = ->
|
||||
event_tracking.sendCountly "ide-open-hotkeys-modal"
|
||||
|
||||
$modal.open {
|
||||
templateUrl: "hotkeysModalTemplate"
|
||||
controller: "HotkeysModalController"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
define [
|
||||
"libs/latex-log-parser"
|
||||
"ide/human-readable-logs/HumanReadableLogsRules"
|
||||
], (LogParser, ruleset) ->
|
||||
parse : (rawLog, options) ->
|
||||
parsedLogEntries = LogParser.parse(rawLog, options)
|
||||
|
||||
_getRule = (logMessage) ->
|
||||
return rule for rule in ruleset when rule.regexToMatch.test logMessage
|
||||
|
||||
for entry in parsedLogEntries.all
|
||||
ruleDetails = _getRule entry.message
|
||||
|
||||
if (ruleDetails?)
|
||||
entry.ruleId = 'hint_' + ruleDetails.regexToMatch.toString().replace(/\s/g, '_').slice(1, -1) if ruleDetails.regexToMatch?
|
||||
|
||||
entry.humanReadableHint = ruleDetails.humanReadableHint if ruleDetails.humanReadableHint?
|
||||
entry.extraInfoURL = ruleDetails.extraInfoURL if ruleDetails.extraInfoURL?
|
||||
|
||||
return parsedLogEntries
|
|
@ -0,0 +1,91 @@
|
|||
define -> [
|
||||
regexToMatch: /Misplaced alignment tab character \&/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Misplaced_alignment_tab_character_%26"
|
||||
humanReadableHint: """
|
||||
You have placed an alignment tab character '&' in the wrong place. If you want to align something, you must write it inside an align environment such as \\begin{align} \u2026 \\end{align}, \\begin{tabular} \u2026 \\end{tabular}, etc. If you want to write an ampersand '&' in text, you must write \\& instead.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Extra alignment tab has been changed to \\cr/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Extra_alignment_tab_has_been_changed_to_%5Ccr"
|
||||
humanReadableHint: """
|
||||
You have written too many alignment tabs in a table, causing one of them to be turned into a line break. Make sure you have specified the correct number of columns in your <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Tables\">table</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Display math should end with \$\$/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Display_math_should_end_with_$$"
|
||||
humanReadableHint: """
|
||||
You have forgotten a $ sign at the end of 'display math' mode. When writing in display math mode, you must always math write inside $$ \u2026 $$. Check that the number of $s match around each math expression.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Missing [{$] inserted./
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Missing_$_inserted"
|
||||
humanReadableHint: """
|
||||
Check that your $'s match around math expressions. If they do, then you've probably used a symbol in normal text that needs to be in math mode. Symbols such as subscripts ( _ ), integrals ( \\int ), Greek letters ( \\alpha, \\beta, \\delta ), and modifiers (\\vec{x}, \\tilde{x} ) must be written in math mode. See the full list <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/Missing_$_inserted \">here</a>.If you intended to use mathematics mode, then use $ \u2026 $ for 'inline math mode', $$ \u2026 $$ for 'display math mode' or alternatively \begin{math} \u2026 \end{math}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /(undefined )?[rR]eference(s)?.+(undefined)?/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_undefined_references"
|
||||
humanReadableHint: """
|
||||
You have referenced something which has not yet been labelled. If you have labelled it already, make sure that what is written inside \\ref{...} is the same as what is written inside \\label{...}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Citation .+ on page .+ undefined on input line .+/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Citation_XXX_on_page_XXX_undefined_on_input_line_XXX"
|
||||
humanReadableHint: """
|
||||
You have cited something which is not included in your bibliography. Make sure that the citation (\\cite{...}) has a corresponding key in your bibliography, and that both are spelled the same way.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /(Label .+)? multiply[ -]defined( labels)?/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/There_were_multiply-defined_labels"
|
||||
humanReadableHint: """
|
||||
You have used the same label more than once. Check that each \\label{...} labels only one item.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /`!?h' float specifier changed to `!?ht'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/%60!h%27_float_specifier_changed_to_%60!ht%27"
|
||||
humanReadableHint: """
|
||||
The float specifier 'h' is too strict of a demand for LaTeX to place your float in a nice way here. Try relaxing it by using 'ht', or even 'htbp' if necessary. If you want to try keep the float here anyway, check out the <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">float package</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /No positions in optional float specifier/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/No_positions_in_optional_float_specifier"
|
||||
humanReadableHint: """
|
||||
You have forgotten to include a float specifier, which tells LaTeX where to position your figure. To fix this, either insert a float specifier inside the square brackets (e.g. \begin{figure}[h]), or remove the square brackets (e.g. \begin{figure}). Find out more about float specifiers <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /Undefined control sequence/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/Undefined_control_sequence"
|
||||
humanReadableHint: """
|
||||
The compiler is having trouble understanding a command you have used. Check that the command is spelled correctly. If the command is part of a package, make sure you have included the package in your preamble using \\usepackage{...}.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /File .+ not found/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX"
|
||||
humanReadableHint: """
|
||||
The compiler cannot find the file you want to include. Make sure that you have <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Including_images_in_ShareLaTeX\">uploaded the file</a> and <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/File_XXX_not_found_on_input_line_XXX.\">specified the file location correctly</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown graphics extension: \..+/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.XXX"
|
||||
humanReadableHint: """
|
||||
The compiler does not recognise the file type of one of your images. Make sure you are using a <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_graphics_extension:_.gif.\">supported image format</a> for your choice of compiler, and check that there are no periods (.) in the name of your image.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown float option `H'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60H%27"
|
||||
humanReadableHint: """
|
||||
The compiler isn't recognizing the float option 'H'. Include \\usepackage{float} in your preamble to fix this.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: Unknown float option `q'/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_Unknown_float_option_%60q%27"
|
||||
humanReadableHint: """
|
||||
You have used a float specifier which the compiler does not understand. You can learn more about the different float options available for placing figures <a target=\"_blank\" href=\"https://www.sharelatex.com/learn/Positioning_of_Figures\">here</a>.
|
||||
"""
|
||||
,
|
||||
regexToMatch: /LaTeX Error: \\math.+ allowed only in math mode/
|
||||
extraInfoURL: "https://www.sharelatex.com/learn/Errors/LaTeX_Error:_%5Cmathrm_allowed_only_in_math_mode"
|
||||
humanReadableHint: """
|
||||
You have used a font command which is only available in math mode. To use this command, you must be in maths mode (E.g. $ \u2026 $ or \\begin{math} \u2026 \\end{math}). If you want to use it outside of math mode, use the text version instead: \\textrm, \\textit, etc.
|
||||
"""
|
||||
]
|
|
@ -1,15 +1,67 @@
|
|||
define [
|
||||
"base"
|
||||
"libs/latex-log-parser"
|
||||
"ace/ace"
|
||||
"ide/human-readable-logs/HumanReadableLogs"
|
||||
"libs/bib-log-parser"
|
||||
], (App, LogParser, BibLogParser) ->
|
||||
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, localStorage) ->
|
||||
"services/log-hints-feedback"
|
||||
], (App, Ace, HumanReadableLogs, BibLogParser) ->
|
||||
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) ->
|
||||
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
autoCompile = true
|
||||
|
||||
# pdf.view = uncompiled | pdf | errors
|
||||
$scope.pdf.view = if $scope?.pdf?.url then 'pdf' else 'uncompiled'
|
||||
$scope.shouldShowLogs = false
|
||||
$scope.wikiEnabled = window.wikiEnabled;
|
||||
|
||||
# view logic to check whether the files dropdown should "drop up" or "drop down"
|
||||
$scope.shouldDropUp = false
|
||||
|
||||
logsContainerEl = document.querySelector ".pdf-logs"
|
||||
filesDropdownEl = logsContainerEl?.querySelector ".files-dropdown"
|
||||
|
||||
# get the top coordinate of the files dropdown as a ratio (to the logs container height)
|
||||
# logs container supports scrollable content, so it's possible that ratio > 1.
|
||||
getFilesDropdownTopCoordAsRatio = () ->
|
||||
filesDropdownEl?.getBoundingClientRect().top / logsContainerEl?.getBoundingClientRect().height
|
||||
|
||||
$scope.$watch "shouldShowLogs", (shouldShow) ->
|
||||
if shouldShow
|
||||
$scope.$applyAsync () ->
|
||||
$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
|
||||
|
||||
# log hints tracking
|
||||
$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
|
||||
|
||||
$scope.trackLogHintsLearnMore = () ->
|
||||
event_tracking.sendCountly "logs-hints-learn-more"
|
||||
|
||||
trackLogHintsFeedback = (isPositive, hintId) ->
|
||||
event_tracking.send "log-hints", (if isPositive then "feedback-positive" else "feedback-negative"), hintId
|
||||
event_tracking.sendCountly (if isPositive then "log-hints-feedback-positive" else "log-hints-feedback-negative"), { hintId }
|
||||
|
||||
$scope.trackLogHintsNegFeedbackDetails = (hintId, feedbackOpt, feedbackOtherVal) ->
|
||||
logHintsFeedback.submitFeedback hintId, feedbackOpt, feedbackOtherVal
|
||||
|
||||
$scope.trackLogHintsPositiveFeedback = (hintId) -> trackLogHintsFeedback true, hintId
|
||||
$scope.trackLogHintsNegativeFeedback = (hintId) -> trackLogHintsFeedback false, hintId
|
||||
|
||||
if ace.require("ace/lib/useragent").isMac
|
||||
$scope.modifierKey = "Cmd"
|
||||
else
|
||||
$scope.modifierKey = "Ctrl"
|
||||
|
||||
# utility for making a query string from a hash, could use jquery $.param
|
||||
createQueryString = (args) ->
|
||||
qs_args = ("#{k}=#{v}" for k, v of args)
|
||||
if qs_args.length then "?" + qs_args.join("&") else ""
|
||||
|
||||
$scope.stripHTMLFromString = (htmlStr) ->
|
||||
tmp = document.createElement("DIV")
|
||||
tmp.innerHTML = htmlStr
|
||||
return tmp.textContent || tmp.innerText || ""
|
||||
|
||||
$scope.$on "project:joined", () ->
|
||||
return if !autoCompile
|
||||
|
@ -28,13 +80,14 @@ define [
|
|||
|
||||
sendCompileRequest = (options = {}) ->
|
||||
url = "/project/#{$scope.project_id}/compile"
|
||||
params = {}
|
||||
if options.isAutoCompile
|
||||
url += "?auto_compile=true"
|
||||
params["auto_compile"]=true
|
||||
return $http.post url, {
|
||||
rootDoc_id: options.rootDocOverride_id or null
|
||||
draft: $scope.draft
|
||||
_csrf: window.csrfToken
|
||||
}
|
||||
}, {params: params}
|
||||
|
||||
parseCompileResponse = (response) ->
|
||||
|
||||
|
@ -42,20 +95,36 @@ define [
|
|||
$scope.pdf.error = false
|
||||
$scope.pdf.timedout = false
|
||||
$scope.pdf.failure = false
|
||||
$scope.pdf.projectTooLarge = false
|
||||
$scope.pdf.url = null
|
||||
$scope.pdf.clsiMaintenance = false
|
||||
$scope.pdf.tooRecentlyCompiled = false
|
||||
$scope.pdf.renderingError = false
|
||||
$scope.pdf.projectTooLarge = false
|
||||
$scope.pdf.compileTerminated = false
|
||||
|
||||
# make a cache to look up files by name
|
||||
fileByPath = {}
|
||||
for file in response.outputFiles
|
||||
if response?.outputFiles?
|
||||
for file in response?.outputFiles
|
||||
fileByPath[file.path] = file
|
||||
|
||||
# prepare query string
|
||||
qs = {}
|
||||
# add a query string parameter for the compile group
|
||||
if response.compileGroup?
|
||||
ide.compileGroup = qs.compileGroup = response.compileGroup
|
||||
# add a query string parameter for the clsi server id
|
||||
if response.clsiServerId?
|
||||
ide.clsiServerId = qs.clsiserverid = response.clsiServerId
|
||||
|
||||
if response.status == "timedout"
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.timedout = true
|
||||
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
|
||||
else if response.status == "terminated"
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.compileTerminated = true
|
||||
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
|
||||
else if response.status == "autocompile-backoff"
|
||||
$scope.pdf.view = 'uncompiled'
|
||||
else if response.status == "project-too-large"
|
||||
|
@ -72,12 +141,13 @@ define [
|
|||
else if response.status == "too-recently-compiled"
|
||||
$scope.pdf.view = 'errors'
|
||||
$scope.pdf.tooRecentlyCompiled = true
|
||||
else if response.status == "validation-problems"
|
||||
$scope.pdf.view = "validation-problems"
|
||||
$scope.pdf.validation = response.validationProblems
|
||||
else if response.status == "success"
|
||||
$scope.pdf.view = 'pdf'
|
||||
$scope.shouldShowLogs = false
|
||||
|
||||
# prepare query string
|
||||
qs = {}
|
||||
# define the base url. if the pdf file has a build number, pass it to the clsi in the url
|
||||
if fileByPath['output.pdf']?.url?
|
||||
$scope.pdf.url = fileByPath['output.pdf'].url
|
||||
|
@ -89,17 +159,11 @@ define [
|
|||
# check if we need to bust cache (build id is unique so don't need it in that case)
|
||||
if not fileByPath['output.pdf']?.build?
|
||||
qs.cache_bust = "#{Date.now()}"
|
||||
# add a query string parameter for the compile group
|
||||
if response.compileGroup?
|
||||
$scope.pdf.compileGroup = response.compileGroup
|
||||
qs.compileGroup = "#{$scope.pdf.compileGroup}"
|
||||
if response.clsiServerId?
|
||||
qs.clsiserverid = response.clsiServerId
|
||||
ide.clsiServerId = response.clsiServerId
|
||||
# convert the qs hash into a query string and append it
|
||||
qs_args = ("#{k}=#{v}" for k, v of qs)
|
||||
$scope.pdf.qs = if qs_args.length then "?" + qs_args.join("&") else ""
|
||||
$scope.pdf.url += $scope.pdf.qs
|
||||
$scope.pdf.url += createQueryString qs
|
||||
# Save all downloads as files
|
||||
qs.popupDownload = true
|
||||
$scope.pdf.downloadUrl = "/project/#{$scope.project_id}/output/output.pdf" + createQueryString(qs)
|
||||
|
||||
fetchLogs(fileByPath['output.log'], fileByPath['output.blg'])
|
||||
|
||||
|
@ -108,17 +172,18 @@ define [
|
|||
|
||||
if !response.outputFiles?
|
||||
return
|
||||
|
||||
# prepare list of output files for download dropdown
|
||||
qs = {}
|
||||
if response.clsiServerId?
|
||||
qs.clsiserverid = response.clsiServerId
|
||||
for file in response.outputFiles
|
||||
if IGNORE_FILES.indexOf(file.path) == -1
|
||||
$scope.pdf.outputFiles.push {
|
||||
# Turn 'output.blg' into 'blg file'.
|
||||
if file.path.match(/^output\./)
|
||||
file.name = "#{file.path.replace(/^output\./, "")} file"
|
||||
else
|
||||
file.name = file.path
|
||||
file.url = "/project/#{project_id}/output/#{file.path}"
|
||||
if response.clsiServerId?
|
||||
file.url = file.url + "?clsiserverid=#{response.clsiServerId}"
|
||||
$scope.pdf.outputFiles.push file
|
||||
name: if file.path.match(/^output\./) then "#{file.path.replace(/^output\./, "")} file" else file.path
|
||||
url: "/project/#{project_id}/output/#{file.path}" + createQueryString qs
|
||||
}
|
||||
|
||||
|
||||
fetchLogs = (logFile, blgFile) ->
|
||||
|
@ -127,14 +192,17 @@ define [
|
|||
opts =
|
||||
method:"GET"
|
||||
params:
|
||||
build:file.build
|
||||
compileGroup:ide.compileGroup
|
||||
clsiserverid:ide.clsiServerId
|
||||
if file.url? # FIXME clean this up when we have file.urls out consistently
|
||||
if file?.url? # FIXME clean this up when we have file.urls out consistently
|
||||
opts.url = file.url
|
||||
else if file?.build?
|
||||
opts.url = "/project/#{$scope.project_id}/build/#{file.build}/output/#{name}"
|
||||
else
|
||||
opts.url = "/project/#{$scope.project_id}/output/#{name}"
|
||||
# check if we need to bust cache (build id is unique so don't need it in that case)
|
||||
if not file?.build?
|
||||
opts.params.cache_bust = "#{Date.now()}"
|
||||
return $http(opts)
|
||||
|
||||
# accumulate the log entries
|
||||
|
@ -150,7 +218,7 @@ define [
|
|||
# use the parsers for each file type
|
||||
processLog = (log) ->
|
||||
$scope.pdf.rawLog = log
|
||||
{errors, warnings, typesetting} = LogParser.parse(log, ignoreDuplicates: true)
|
||||
{errors, warnings, typesetting} = HumanReadableLogs.parse(log, ignoreDuplicates: true)
|
||||
all = [].concat errors, warnings, typesetting
|
||||
accumulateResults {all, errors, warnings}
|
||||
|
||||
|
@ -204,7 +272,7 @@ define [
|
|||
return null
|
||||
|
||||
normalizeFilePath = (path) ->
|
||||
path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
|
||||
path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}(-[0-9a-f]{24})?\/(\.\/)?/, "")
|
||||
path = path.replace(/^\/compile\//, "")
|
||||
|
||||
rootDocDirname = ide.fileTreeManager.getRootDocDirname()
|
||||
|
@ -215,6 +283,9 @@ define [
|
|||
|
||||
$scope.recompile = (options = {}) ->
|
||||
return if $scope.pdf.compiling
|
||||
|
||||
event_tracking.sendCountlySampled "editor-recompile-sampled", options
|
||||
|
||||
$scope.pdf.compiling = true
|
||||
|
||||
ide.$scope.$broadcast("flush-changes")
|
||||
|
@ -234,6 +305,21 @@ define [
|
|||
|
||||
# This needs to be public.
|
||||
ide.$scope.recompile = $scope.recompile
|
||||
# This method is a simply wrapper and exists only for tracking purposes.
|
||||
ide.$scope.recompileViaKey = () ->
|
||||
$scope.recompile { keyShortcut: true }
|
||||
|
||||
$scope.stop = () ->
|
||||
return if !$scope.pdf.compiling
|
||||
|
||||
$http {
|
||||
url: "/project/#{$scope.project_id}/compile/stop"
|
||||
method: "POST"
|
||||
params:
|
||||
clsiserverid:ide.clsiServerId
|
||||
headers:
|
||||
"X-Csrf-Token": window.csrfToken
|
||||
}
|
||||
|
||||
$scope.clearCache = () ->
|
||||
$http {
|
||||
|
@ -247,6 +333,7 @@ define [
|
|||
|
||||
$scope.toggleLogs = () ->
|
||||
$scope.shouldShowLogs = !$scope.shouldShowLogs
|
||||
event_tracking.sendCountlyOnce "ide-open-logs-once" if $scope.shouldShowLogs
|
||||
|
||||
$scope.showPdf = () ->
|
||||
$scope.pdf.view = "pdf"
|
||||
|
@ -254,6 +341,7 @@ define [
|
|||
|
||||
$scope.toggleRawLog = () ->
|
||||
$scope.pdf.showRawLog = !$scope.pdf.showRawLog
|
||||
event_tracking.sendCountly "logs-view-raw" if $scope.pdf.showRawLog
|
||||
|
||||
$scope.openClearCacheModal = () ->
|
||||
modalInstance = $modal.open(
|
||||
|
@ -287,10 +375,16 @@ define [
|
|||
|
||||
$scope.startFreeTrial = (source) ->
|
||||
ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source)
|
||||
|
||||
event_tracking.sendCountly "subscription-start-trial", { source }
|
||||
|
||||
window.open("/user/subscription/new?planCode=student_free_trial_7_days")
|
||||
$scope.startedFreeTrial = true
|
||||
|
||||
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
|
||||
# enable per-user containers by default
|
||||
perUserCompile = true
|
||||
|
||||
synctex =
|
||||
syncToPdf: (cursorPosition) ->
|
||||
deferred = $q.defer()
|
||||
|
@ -339,18 +433,35 @@ define [
|
|||
deferred.reject()
|
||||
return deferred.promise
|
||||
|
||||
# FIXME: this actually works better if it's halfway across the
|
||||
# page (or the visible part of the page). Synctex doesn't
|
||||
# always find the right place in the file when the point is at
|
||||
# the edge of the page, it sometimes returns the start of the
|
||||
# next paragraph instead.
|
||||
h = position.offset.left
|
||||
|
||||
# Compute the vertical position to pass to synctex, which
|
||||
# works with coordinates increasing from the top of the page
|
||||
# down. This matches the browser's DOM coordinate of the
|
||||
# click point, but the pdf position is measured from the
|
||||
# bottom of the page so we need to invert it.
|
||||
if options.fromPdfPosition and position.pageSize?.height?
|
||||
v = (position.pageSize.height - position.offset.top) or 0 # measure from pdf point (inverted)
|
||||
else
|
||||
v = position.offset.top or 0 # measure from html click position
|
||||
|
||||
# It's not clear exactly where we should sync to if it wasn't directly
|
||||
# clicked on, but a little bit down from the very top seems best.
|
||||
if options.includeVisualOffset
|
||||
position.offset.top = position.offset.top + 80
|
||||
v += 72 # use the same value as in pdfViewer highlighting visual offset
|
||||
|
||||
$http({
|
||||
url: "/project/#{ide.project_id}/sync/pdf",
|
||||
method: "GET",
|
||||
params: {
|
||||
page: position.page + 1
|
||||
h: position.offset.left.toFixed(2)
|
||||
v: position.offset.top.toFixed(2)
|
||||
h: h.toFixed(2)
|
||||
v: v.toFixed(2)
|
||||
clsiserverid:ide.clsiServerId
|
||||
}
|
||||
})
|
||||
|
@ -380,14 +491,15 @@ define [
|
|||
|
||||
$scope.syncToCode = () ->
|
||||
synctex
|
||||
.syncToCode($scope.pdf.position, includeVisualOffset: true)
|
||||
.syncToCode($scope.pdf.position, includeVisualOffset: true, fromPdfPosition: true)
|
||||
.then (data) ->
|
||||
{doc, line} = data
|
||||
ide.editorManager.openDoc(doc, gotoLine: line)
|
||||
]
|
||||
|
||||
App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
|
||||
App.controller "PdfLogEntryController", ["$scope", "ide", "event_tracking", ($scope, ide, event_tracking) ->
|
||||
$scope.openInEditor = (entry) ->
|
||||
event_tracking.sendCountlyOnce "logs-jump-to-location-once"
|
||||
entity = ide.fileTreeManager.findEntityByPath(entry.file)
|
||||
return if !entity? or entity.type != "doc"
|
||||
if entry.line?
|
||||
|
|
|
@ -1,23 +1,14 @@
|
|||
define [
|
||||
"base"
|
||||
"ide/pdfng/directives/pdfViewer"
|
||||
"text!libs/pdfListView/TextLayer.css"
|
||||
"text!libs/pdfListView/AnnotationsLayer.css"
|
||||
"text!libs/pdfListView/HighlightsLayer.css"
|
||||
], (
|
||||
App
|
||||
pdfViewer
|
||||
textLayerCss
|
||||
annotationsLayerCss
|
||||
highlightsLayerCss
|
||||
|
||||
) ->
|
||||
if PDFJS?
|
||||
PDFJS.workerSrc = window.pdfJsWorkerPath
|
||||
|
||||
style = $("<style/>")
|
||||
style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
|
||||
$("body").append(style)
|
||||
|
||||
App.directive "pdfng", ["$timeout", "localStorage", ($timeout, localStorage) ->
|
||||
return {
|
||||
scope: {
|
||||
|
@ -27,15 +18,6 @@ define [
|
|||
"dblClickCallback": "="
|
||||
}
|
||||
link: (scope, element, attrs) ->
|
||||
# pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
|
||||
# textLayerBuilder: TextLayerBuilder
|
||||
# annotationsLayerBuilder: AnnotationsLayerBuilder
|
||||
# highlightsLayerBuilder: HighlightsLayerBuilder
|
||||
# ondblclick: (e) -> onDoubleClick(e)
|
||||
# # logLevel: PDFListView.Logger.DEBUG
|
||||
# pdfListView.listView.pageWidthOffset = 20
|
||||
# pdfListView.listView.pageHeightOffset = 20
|
||||
|
||||
scope.loading = false
|
||||
scope.pleaseJumpTo = null
|
||||
scope.scale = null
|
||||
|
@ -50,9 +32,11 @@ define [
|
|||
scope.scale = { scaleMode: 'scale_mode_fit_width' }
|
||||
|
||||
if (position = localStorage("pdf.position.#{attrs.key}"))
|
||||
scope.position = { page: +position.page, offset: { "top": +position.offset.top, "left": +position.offset.left } }
|
||||
|
||||
#scope.position = pdfListView.getPdfPosition(true)
|
||||
scope.position =
|
||||
page: +position.page,
|
||||
offset:
|
||||
"top": +position.offset.top
|
||||
"left": +position.offset.left
|
||||
|
||||
scope.$on "$destroy", () =>
|
||||
localStorage "pdf.scale", scope.scale
|
||||
|
@ -78,56 +62,18 @@ define [
|
|||
scope.$watch "pdfSrc", (url) ->
|
||||
if url
|
||||
scope.loading = true
|
||||
# console.log 'pdfSrc =', url
|
||||
scope.loaded = false
|
||||
scope.progress = 1
|
||||
initializePosition()
|
||||
flashControls()
|
||||
# pdfListView
|
||||
# .loadPdf(url, onProgress)
|
||||
# .then () ->
|
||||
# scope.$apply () ->
|
||||
# scope.loading = false
|
||||
# delete scope.progress
|
||||
# initializePosition()
|
||||
# flashControls()
|
||||
|
||||
scope.$on "loaded", () ->
|
||||
scope.loaded = true
|
||||
scope.progress = 100
|
||||
scope.$apply()
|
||||
$timeout () ->
|
||||
scope.loading = false
|
||||
delete scope.progress
|
||||
, 250
|
||||
|
||||
#scope.$watch "highlights", (areas) ->
|
||||
# console.log 'got HIGHLIGHTS in pdfJS', areas
|
||||
|
||||
# return if !areas?
|
||||
# highlights = for area in areas or []
|
||||
# {
|
||||
# page: area.page
|
||||
# highlight:
|
||||
# left: area.h
|
||||
# top: area.v
|
||||
# height: area.height
|
||||
# width: area.width
|
||||
# }
|
||||
|
||||
# if highlights.length > 0
|
||||
# first = highlights[0]
|
||||
# position = {
|
||||
# page: first.page
|
||||
# offset:
|
||||
# left: first.highlight.left
|
||||
# top: first.highlight.top - 80
|
||||
# }
|
||||
# console.log 'position is', position, 'in highlights'
|
||||
# scope.pleaseJumpTo = position
|
||||
# pdfListView.clearHighlights()
|
||||
# pdfListView.setHighlights(highlights, true)
|
||||
|
||||
# setTimeout () =>
|
||||
# pdfListView.clearHighlights()
|
||||
# , 1000
|
||||
, 500
|
||||
|
||||
scope.fitToHeight = () ->
|
||||
scale = angular.copy (scope.scale)
|
||||
|
@ -155,10 +101,10 @@ define [
|
|||
for event in attrs.resizeOn.split(",")
|
||||
scope.$on event, (e) ->
|
||||
#console.log 'got a resize event', event, e
|
||||
#
|
||||
|
||||
scope.$on 'progress', (event, progress) ->
|
||||
scope.$apply () ->
|
||||
return if scope.loaded
|
||||
scope.progress = Math.floor(progress.loaded/progress.total*100)
|
||||
scope.progress = 100 if scope.progress > 100
|
||||
scope.progress = 0 if scope.progress < 0
|
||||
|
|
|
@ -3,7 +3,7 @@ define [
|
|||
], (App) ->
|
||||
# App = angular.module 'pdfPage', ['pdfHighlights']
|
||||
|
||||
App.directive 'pdfPage', ['$timeout', 'pdfHighlights', ($timeout, pdfHighlights) ->
|
||||
App.directive 'pdfPage', ['$timeout', 'pdfHighlights', 'pdfSpinner', ($timeout, pdfHighlights, pdfSpinner) ->
|
||||
{
|
||||
require: '^pdfViewer',
|
||||
template: '''
|
||||
|
@ -82,7 +82,7 @@ define [
|
|||
if scope.timeoutHandler
|
||||
$timeout.cancel(scope.timeoutHandler)
|
||||
highlightsLayer.clearHighlights()
|
||||
scope.timeoutHandler
|
||||
scope.timeoutHandler = null
|
||||
|
||||
# console.log 'got highlight watch in pdfPage', scope.page
|
||||
pageHighlights = (h for h in highlights when h.page == scope.page.pageNum)
|
||||
|
|
|
@ -8,11 +8,13 @@ define [
|
|||
class PDFRenderer
|
||||
JOB_QUEUE_INTERVAL: 25
|
||||
PAGE_LOAD_TIMEOUT: 60*1000
|
||||
PAGE_RENDER_TIMEOUT: 60*1000
|
||||
INDICATOR_DELAY1: 100 # time to delay before showing the indicator
|
||||
INDICATOR_DELAY2: 250 # time until the indicator starts animating
|
||||
|
||||
constructor: (@url, @options) ->
|
||||
# PDFJS.disableFontFace = true # avoids repaints, uses worker more
|
||||
# PDFJS.disableAutoFetch = true # enable this to prevent loading whole file
|
||||
if @options.disableAutoFetch
|
||||
PDFJS.disableAutoFetch = true # prevent loading whole file
|
||||
# PDFJS.disableStream
|
||||
# PDFJS.disableRange
|
||||
@scale = @options.scale || 1
|
||||
|
@ -28,15 +30,22 @@ define [
|
|||
@errorCallback = @options.errorCallback
|
||||
@pageSizeChangeCallback = @options.pageSizeChangeCallback
|
||||
@pdfjs.promise.catch (exception) =>
|
||||
# console.log 'ERROR in get document', exception
|
||||
# error getting document
|
||||
@errorCallback(exception)
|
||||
|
||||
resetState: () ->
|
||||
@complete = []
|
||||
@timeout = []
|
||||
@pageLoad = []
|
||||
@renderTask = []
|
||||
@renderQueue = []
|
||||
clearTimeout @queueTimer if @queueTimer?
|
||||
# clear any existing timers, render tasks
|
||||
for timer in @spinTimer or []
|
||||
clearTimeout timer
|
||||
for page in @pageState or []
|
||||
page?.loadTask?.cancel()
|
||||
page?.renderTask?.cancel()
|
||||
# initialise/reset the state
|
||||
@pageState = []
|
||||
@spinTimer = [] # timers for starting the spinners (to avoid jitter)
|
||||
@spinTimerDone = [] # array of pages where the spinner has activated
|
||||
@jobs = 0
|
||||
|
||||
getNumPages: () ->
|
||||
|
@ -45,7 +54,6 @@ define [
|
|||
|
||||
getPage: (pageNum) ->
|
||||
@document.then (pdfDocument) ->
|
||||
# console.log 'got pdf document, now getting Page', pageNum
|
||||
pdfDocument.getPage(pageNum)
|
||||
|
||||
getPdfViewport: (pageNum, scale) ->
|
||||
|
@ -80,14 +88,14 @@ define [
|
|||
@resetState()
|
||||
|
||||
triggerRenderQueue: (interval = @JOB_QUEUE_INTERVAL) ->
|
||||
if @queueTimer?
|
||||
clearTimeout @queueTimer
|
||||
@queueTimer = setTimeout () =>
|
||||
@queueTimer = null
|
||||
@processRenderQueue()
|
||||
, interval
|
||||
|
||||
removeCompletedJob: (taskRef, pagenum) ->
|
||||
# may need to clean up deferred object here
|
||||
delete taskRef[pagenum]
|
||||
removeCompletedJob: (pagenum) ->
|
||||
@jobs = @jobs - 1
|
||||
@triggerRenderQueue(0)
|
||||
|
||||
|
@ -109,69 +117,129 @@ define [
|
|||
@renderQueue.push current
|
||||
@processRenderQueue()
|
||||
|
||||
getPageDetails: (page) ->
|
||||
return [page.element.canvas, page.pagenum]
|
||||
|
||||
# handle the loading indicators for each page
|
||||
|
||||
startIndicators: () ->
|
||||
# make an array of the pages in the queue
|
||||
@queuedPages = []
|
||||
@queuedPages[page.pagenum] = true for page in @renderQueue
|
||||
# clear any unfinished spinner timers on pages that aren't in the queue any more
|
||||
for pagenum of @spinTimer when not @queuedPages[pagenum]
|
||||
clearTimeout @spinTimer[pagenum]
|
||||
delete @spinTimer[pagenum]
|
||||
# add indicators for any new pages in the current queue
|
||||
for page in @renderQueue when not @spinTimer[page.pagenum] and not @spinTimerDone[page.pagenum]
|
||||
@startIndicator page
|
||||
|
||||
startIndicator: (page) ->
|
||||
[canvas, pagenum] = @getPageDetails page
|
||||
canvas.addClass('pdfng-loading')
|
||||
@spinTimer[pagenum] = setTimeout () =>
|
||||
for queuedPage in @renderQueue
|
||||
if pagenum == queuedPage.pagenum
|
||||
@spinner.add(canvas, {static:true})
|
||||
@spinTimerDone[pagenum] = true
|
||||
break
|
||||
delete @spinTimer[pagenum]
|
||||
, @INDICATOR_DELAY1
|
||||
|
||||
updateIndicator: (page) ->
|
||||
[canvas, pagenum] = @getPageDetails page
|
||||
# did the spinner insert itself already?
|
||||
if @spinTimerDone[pagenum]
|
||||
@spinTimer[pagenum] = setTimeout () =>
|
||||
@spinner.start(canvas)
|
||||
delete @spinTimer[pagenum]
|
||||
, @INDICATOR_DELAY2
|
||||
else
|
||||
# stop the existing spin timer
|
||||
clearTimeout @spinTimer[pagenum]
|
||||
# start a new one which will also start spinning
|
||||
@spinTimer[pagenum] = setTimeout () =>
|
||||
@spinner.add(canvas, {static:true})
|
||||
@spinTimerDone[pagenum] = true
|
||||
@spinTimer[pagenum] = setTimeout () =>
|
||||
@spinner.start(canvas)
|
||||
delete @spinTimer[pagenum]
|
||||
, @INDICATOR_DELAY2
|
||||
, @INDICATOR_DELAY1
|
||||
|
||||
clearIndicator: (page) ->
|
||||
[canvas, pagenum] = @getPageDetails page
|
||||
@spinner.stop(canvas)
|
||||
clearTimeout @spinTimer[pagenum]
|
||||
delete @spinTimer[pagenum]
|
||||
@spinTimerDone[pagenum] = true
|
||||
|
||||
# handle the queue of pages to be rendered
|
||||
|
||||
processRenderQueue: () ->
|
||||
return if @shuttingDown
|
||||
# mark all pages in the queue as loading
|
||||
@startIndicators()
|
||||
# bail out if there is already a render job running
|
||||
return if @jobs > 0
|
||||
current = @renderQueue.shift()
|
||||
return unless current?
|
||||
[element, pagenum] = [current.element, current.pagenum]
|
||||
# if task is underway or complete, go to the next entry in the
|
||||
# render queue
|
||||
# console.log 'processing renderq', pagenum, @renderTask[pagenum], @complete[pagenum]
|
||||
if @pageLoad[pagenum] or @renderTask[pagenum] or @complete[pagenum]
|
||||
@processRenderQueue()
|
||||
return
|
||||
# take the first page in the queue
|
||||
page = @renderQueue.shift()
|
||||
# check if it is in action already
|
||||
while page? and @pageState[page.pagenum]?
|
||||
page = @renderQueue.shift()
|
||||
return unless page?
|
||||
[element, pagenum] = [page.element, page.pagenum]
|
||||
@jobs = @jobs + 1
|
||||
|
||||
element.canvas.addClass('pdfng-loading')
|
||||
spinTimer = setTimeout () =>
|
||||
@spinner.add(element.canvas)
|
||||
, 100
|
||||
|
||||
completeRef = @complete
|
||||
renderTaskRef = @renderTask
|
||||
# console.log 'started page load', pagenum
|
||||
# update the spinner to make it spinning (signifies loading has begun)
|
||||
@updateIndicator page
|
||||
|
||||
timedOut = false
|
||||
timer = $timeout () =>
|
||||
Raven?.captureMessage?('pdfng page load timed out after ' + @PAGE_LOAD_TIMEOUT + 'ms (1% sample)') if Math.random() < 0.01
|
||||
# console.log 'page load timed out', pagenum
|
||||
timer = $timeout () => # page load timed out
|
||||
return if loadTask.cancelled # return from cancelled page load
|
||||
Raven?.captureMessage?('pdfng page load timed out after ' + @PAGE_LOAD_TIMEOUT + 'ms')
|
||||
timedOut = true
|
||||
clearTimeout(spinTimer)
|
||||
@spinner.stop(element.canvas)
|
||||
@clearIndicator page
|
||||
# @jobs = @jobs - 1
|
||||
# @triggerRenderQueue(0)
|
||||
@errorCallback?('timeout')
|
||||
, @PAGE_LOAD_TIMEOUT
|
||||
|
||||
@pageLoad[pagenum] = @getPage(pagenum)
|
||||
loadTask = @getPage(pagenum)
|
||||
|
||||
@pageLoad[pagenum].then (pageObject) =>
|
||||
# console.log 'in page load success', pagenum
|
||||
loadTask.cancel = () ->
|
||||
@cancelled = true
|
||||
|
||||
@pageState[pagenum] = pageState = { loadTask: loadTask }
|
||||
|
||||
loadTask.then (pageObject) =>
|
||||
# page load success
|
||||
$timeout.cancel(timer)
|
||||
clearTimeout(spinTimer)
|
||||
@renderTask[pagenum] = @doRender element, pagenum, pageObject
|
||||
@renderTask[pagenum].then () =>
|
||||
# complete
|
||||
# console.log 'render task success', pagenum
|
||||
completeRef[pagenum] = true
|
||||
@removeCompletedJob renderTaskRef, pagenum
|
||||
return if loadTask.cancelled # return from cancelled page load
|
||||
pageState.renderTask = @doRender element, pagenum, pageObject
|
||||
pageState.renderTask.then () =>
|
||||
# render task success
|
||||
@clearIndicator page
|
||||
pageState.complete = true
|
||||
delete pageState.renderTask
|
||||
@removeCompletedJob pagenum
|
||||
, () =>
|
||||
# console.log 'render task failed', pagenum
|
||||
# rejected
|
||||
@removeCompletedJob renderTaskRef, pagenum
|
||||
.catch (error) ->
|
||||
# console.log 'in page load error', pagenum, 'timedOut=', timedOut
|
||||
# render task failed
|
||||
# could display an error icon
|
||||
pageState.complete = false
|
||||
delete pageState.renderTask
|
||||
@removeCompletedJob pagenum
|
||||
.catch (error) =>
|
||||
# page load error
|
||||
$timeout.cancel(timer)
|
||||
clearTimeout(spinTimer)
|
||||
# console.log 'ERROR', error
|
||||
@clearIndicator page
|
||||
|
||||
doRender: (element, pagenum, page) ->
|
||||
self = this
|
||||
scale = @scale
|
||||
|
||||
if (not scale?)
|
||||
# console.log 'scale is undefined, returning'
|
||||
# scale is undefined, returning
|
||||
return
|
||||
|
||||
canvas = $('<canvas class="pdf-canvas pdfng-rendering"></canvas>')
|
||||
|
@ -220,28 +288,15 @@ define [
|
|||
navigateFn: @navigateFn
|
||||
})
|
||||
|
||||
element.canvas.replaceWith(canvas)
|
||||
|
||||
# console.log 'staring page render', pagenum
|
||||
|
||||
result = page.render {
|
||||
canvasContext: ctx
|
||||
viewport: viewport
|
||||
transform: [pixelRatio, 0, 0, pixelRatio, 0, 0]
|
||||
}
|
||||
|
||||
timedOut = false
|
||||
|
||||
timer = $timeout () =>
|
||||
Raven?.captureMessage?('pdfng page render timed out after ' + @PAGE_RENDER_TIMEOUT + 'ms (1% sample)') if Math.random() < 0.01
|
||||
# console.log 'page render timed out', pagenum
|
||||
timedOut = true
|
||||
result.cancel()
|
||||
, @PAGE_RENDER_TIMEOUT
|
||||
|
||||
result.then () ->
|
||||
# console.log 'page rendered', pagenum
|
||||
$timeout.cancel(timer)
|
||||
# page render success
|
||||
element.canvas.replaceWith(canvas)
|
||||
canvas.removeClass('pdfng-rendering')
|
||||
page.getTextContent().then (textContent) ->
|
||||
textLayer.setTextContent textContent
|
||||
|
@ -252,24 +307,17 @@ define [
|
|||
, (error) ->
|
||||
self.errorCallback?(error)
|
||||
.catch (error) ->
|
||||
# console.log 'page render failed', pagenum, error
|
||||
$timeout.cancel(timer)
|
||||
if timedOut
|
||||
# console.log 'calling ERROR callback - was timeout'
|
||||
self.errorCallback?('timeout')
|
||||
else if error != 'cancelled'
|
||||
# console.log 'calling ERROR callback'
|
||||
# page render failed
|
||||
if error is 'cancelled'
|
||||
return # do nothing when cancelled
|
||||
else
|
||||
self.errorCallback?(error)
|
||||
|
||||
return result
|
||||
|
||||
destroy: () ->
|
||||
# console.log 'in pdf renderer destroy', @renderQueue
|
||||
@shuttingDown = true
|
||||
clearTimeout @queueTimer if @queueTimer?
|
||||
@renderQueue = []
|
||||
for task in @renderTask
|
||||
task.cancel() if task?
|
||||
@resetState()
|
||||
@pdfjs.then (document) ->
|
||||
document.cleanup()
|
||||
document.destroy()
|
||||
|
|
|
@ -8,14 +8,15 @@ define [
|
|||
constructor: () ->
|
||||
# handler for spinners
|
||||
|
||||
add: (element) ->
|
||||
h = element.height()
|
||||
w = element.width()
|
||||
add: (element, options) ->
|
||||
size = 64
|
||||
spinner = $('<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner fa-spin" style="color: #999"></i></div>')
|
||||
spinner = $('<div class="pdfng-spinner" style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(-50%);"><i class="fa fa-spinner' + (if options?.static then '' else ' fa-spin') + '" style="color: #999"></i></div>')
|
||||
spinner.css({'font-size' : size + 'px'})
|
||||
element.append(spinner)
|
||||
|
||||
start: (element) ->
|
||||
element.find('.fa-spinner').addClass('fa-spin')
|
||||
|
||||
stop: (element) ->
|
||||
element.find('.fa-spinner').removeClass('fa-spin')
|
||||
|
||||
|
|
|
@ -27,8 +27,15 @@ define [
|
|||
$scope.document.destroy() if $scope.document?
|
||||
$scope.loadCount = if $scope.loadCount? then $scope.loadCount + 1 else 1
|
||||
# TODO need a proper url manipulation library to add to query string
|
||||
$scope.document = new PDFRenderer($scope.pdfSrc + '&pdfng=true' , {
|
||||
url = $scope.pdfSrc
|
||||
# add 'pdfng=true' to show that we are using the angular pdfjs viewer
|
||||
queryStringExists = url.match(/\?/)
|
||||
url = url + (if not queryStringExists then '?' else '&') + 'pdfng=true'
|
||||
# for isolated compiles, load the pdf on-demand because nobody will overwrite it
|
||||
onDemandLoading = true
|
||||
$scope.document = new PDFRenderer(url, {
|
||||
scale: 1,
|
||||
disableAutoFetch: if onDemandLoading then true else undefined
|
||||
navigateFn: (ref) ->
|
||||
# this function captures clicks on the annotation links
|
||||
$scope.navigateTo = ref
|
||||
|
@ -38,7 +45,7 @@ define [
|
|||
loadedCallback: () ->
|
||||
$scope.$emit 'loaded'
|
||||
errorCallback: (error) ->
|
||||
Raven?.captureMessage?('pdfng error ' + error + ' (1% sample)') if Math.random() < 0.01
|
||||
Raven?.captureMessage?('pdfng error ' + error)
|
||||
$scope.$emit 'pdf:error', error
|
||||
pageSizeChangeCallback: (pageNum, deltaH) ->
|
||||
$scope.$broadcast 'pdf:page:size-change', pageNum, deltaH
|
||||
|
@ -57,6 +64,7 @@ define [
|
|||
result.pdfViewport.width
|
||||
]
|
||||
# console.log 'resolved q.all, page size is', result
|
||||
$scope.$emit 'loaded'
|
||||
$scope.numPages = result.numPages
|
||||
.catch (error) ->
|
||||
$scope.$emit 'pdf:error', error
|
||||
|
@ -187,7 +195,8 @@ define [
|
|||
# console.log 'converted to offset = ', pdfOffset
|
||||
newPosition = {
|
||||
"page": topPageIdx,
|
||||
"offset" : { "top" : pdfOffset[1], "left": 0 }
|
||||
"offset" : { "top" : pdfOffset[1], "left": 0}
|
||||
"pageSize": { "height": viewport.viewBox[3], "width": viewport.viewBox[2] }
|
||||
}
|
||||
return newPosition
|
||||
|
||||
|
@ -208,6 +217,8 @@ define [
|
|||
return $scope.document.getPdfViewport(page.pageNum).then (viewport) ->
|
||||
page.viewport = viewport
|
||||
pageOffset = viewport.convertToViewportPoint(offset.left, offset.top)
|
||||
# if the passed-in position doesn't have the page height/width add them now
|
||||
position.pageSize ?= {"height": viewport.viewBox[3], "width": viewport.viewBox[2]}
|
||||
# console.log 'addition offset =', pageOffset
|
||||
# console.log 'total', pageTop + pageOffset[1]
|
||||
Math.round(pageTop + pageOffset[1] + currentScroll) ## 10 is margin
|
||||
|
@ -246,8 +257,8 @@ define [
|
|||
# console.log 'layoutReady was resolved'
|
||||
|
||||
renderVisiblePages = () ->
|
||||
pages = getVisiblePages()
|
||||
# pages = getExtraPages visiblePages
|
||||
visiblePages = getVisiblePages()
|
||||
pages = getExtraPages visiblePages
|
||||
scope.document.renderPages(pages)
|
||||
|
||||
getVisiblePages = () ->
|
||||
|
@ -264,6 +275,7 @@ define [
|
|||
|
||||
getExtraPages = (visiblePages) ->
|
||||
extra = []
|
||||
if visiblePages.length > 0
|
||||
firstVisiblePage = visiblePages[0].pageNum
|
||||
firstVisiblePageIdx = firstVisiblePage - 1
|
||||
len = visiblePages.length
|
||||
|
@ -513,8 +525,17 @@ define [
|
|||
|
||||
first = highlights[0]
|
||||
|
||||
pageNum = scope.pages[first.page].pageNum
|
||||
# switching between split and full pdf views can cause
|
||||
# highlights to appear before rendering
|
||||
if !scope.pages
|
||||
return # ignore highlight scroll if still rendering
|
||||
|
||||
pageNum = scope.pages[first.page]?.pageNum
|
||||
|
||||
if !pageNum?
|
||||
return # ignore highlight scroll if page not found
|
||||
|
||||
# use a visual offset of 72pt to match the offset in PdfController syncToCode
|
||||
scope.document.getPdfViewport(pageNum).then (viewport) ->
|
||||
position = {
|
||||
page: first.page
|
||||
|
|
|
@ -1,19 +1,43 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "settings", ["ide", (ide) ->
|
||||
App.factory "settings", ["ide", "event_tracking", (ide, event_tracking) ->
|
||||
return {
|
||||
saveSettings: (data) ->
|
||||
# Tracking code.
|
||||
for key in Object.keys(data)
|
||||
changedSetting = key
|
||||
changedSettingVal = data[key]
|
||||
event_tracking.sendCountly "setting-changed", { changedSetting, changedSettingVal }
|
||||
# End of tracking code.
|
||||
|
||||
data._csrf = window.csrfToken
|
||||
ide.$http.post "/user/settings", data
|
||||
|
||||
|
||||
saveProjectSettings: (data) ->
|
||||
# Tracking code.
|
||||
for key in Object.keys(data)
|
||||
changedSetting = key
|
||||
changedSettingVal = data[key]
|
||||
event_tracking.sendCountly "project-setting-changed", { changedSetting, changedSettingVal}
|
||||
# End of tracking code.
|
||||
|
||||
data._csrf = window.csrfToken
|
||||
ide.$http.post "/project/#{ide.project_id}/settings", data
|
||||
|
||||
|
||||
saveProjectAdminSettings: (data) ->
|
||||
# Tracking code.
|
||||
for key in Object.keys(data)
|
||||
changedSetting = key
|
||||
changedSettingVal = data[key]
|
||||
event_tracking.sendCountly "project-admin-setting-changed", { changedSetting, changedSettingVal }
|
||||
# End of tracking code.
|
||||
|
||||
data._csrf = window.csrfToken
|
||||
ide.$http.post "/project/#{ide.project_id}/settings/admin", data
|
||||
|
||||
|
||||
}
|
||||
]
|
|
@ -1,8 +1,10 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "ShareController", ["$scope", "$modal", ($scope, $modal) ->
|
||||
App.controller "ShareController", ["$scope", "$modal", "event_tracking", ($scope, $modal, event_tracking) ->
|
||||
$scope.openShareProjectModal = () ->
|
||||
event_tracking.sendCountlyOnce "ide-open-share-modal-once"
|
||||
|
||||
$modal.open(
|
||||
templateUrl: "shareProjectModalTemplate"
|
||||
controller: "ShareProjectModalController"
|
||||
|
|
|
@ -16,3 +16,4 @@ define [
|
|||
"libs/angular-sixpack"
|
||||
"libs/ng-tags-input-3.0.0"
|
||||
], () ->
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.controller "FreeTrialModalController", ($scope, abTestManager, sixpack)->
|
||||
App.controller "FreeTrialModalController", ($scope, abTestManager, sixpack, event_tracking)->
|
||||
|
||||
$scope.buttonClass = "btn-primary"
|
||||
|
||||
$scope.startFreeTrial = (source, couponCode) ->
|
||||
event_tracking.sendCountly "subscription-start-trial", { source }
|
||||
|
||||
w = window.open()
|
||||
sixpack.convert "track-changes-discount", ->
|
||||
sixpack.participate 'in-editor-free-trial-plan', ['student', 'collaborator'], (planName, rawResponse)->
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
define [
|
||||
"base"
|
||||
"libs/platform"
|
||||
"services/algolia-search"
|
||||
], (App, platform) ->
|
||||
|
||||
|
||||
App.controller 'ContactModal', ($scope, $modal) ->
|
||||
$scope.contactUsModal = () ->
|
||||
modalInstance = $modal.open(
|
||||
|
@ -11,10 +10,25 @@ define [
|
|||
controller: "SupportModalController"
|
||||
)
|
||||
|
||||
App.controller 'SupportModalController', ($scope, $modalInstance) ->
|
||||
App.controller 'SupportModalController', ($scope, $modalInstance, algoliaSearch, event_tracking) ->
|
||||
$scope.form = {}
|
||||
$scope.sent = false
|
||||
$scope.sending = false
|
||||
$scope.suggestions = [];
|
||||
|
||||
_handleSearchResults = (success, results) ->
|
||||
suggestions = for hit in results.hits
|
||||
page_underscored = hit.pageName.replace(/\s/g,'_')
|
||||
|
||||
suggestion =
|
||||
url :"/learn/kb/#{page_underscored}"
|
||||
name : hit._highlightResult.pageName.value
|
||||
|
||||
event_tracking.sendCountly "contact-form-suggestions-shown" if results.hits.length
|
||||
|
||||
$scope.$applyAsync () ->
|
||||
$scope.suggestions = suggestions
|
||||
|
||||
$scope.contactUs = ->
|
||||
if !$scope.form.email?
|
||||
console.log "email not set"
|
||||
|
@ -36,6 +50,18 @@ define [
|
|||
$scope.sent = true
|
||||
$scope.$apply()
|
||||
|
||||
$scope.$watch "form.subject", (newVal, oldVal) ->
|
||||
if newVal and newVal != oldVal and newVal.length > 3
|
||||
algoliaSearch.searchKB newVal, _handleSearchResults, {
|
||||
hitsPerPage: 3
|
||||
typoTolerance: 'strict'
|
||||
}
|
||||
else
|
||||
$scope.suggestions = [];
|
||||
|
||||
$scope.clickSuggestionLink = (url) ->
|
||||
event_tracking.sendCountly "contact-form-suggestions-clicked", { url }
|
||||
|
||||
$scope.close = () ->
|
||||
$modalInstance.close()
|
||||
|
||||
|
|
|
@ -1,17 +1,61 @@
|
|||
define [
|
||||
"base"
|
||||
"modules/localStorage"
|
||||
], (App) ->
|
||||
CACHE_KEY = "countlyEvents"
|
||||
|
||||
App.factory "event_tracking", (localStorage) ->
|
||||
_getEventCache = () ->
|
||||
eventCache = localStorage CACHE_KEY
|
||||
|
||||
# Initialize as an empy object if the event cache is still empty.
|
||||
if !eventCache?
|
||||
eventCache = {}
|
||||
localStorage CACHE_KEY, eventCache
|
||||
|
||||
return eventCache
|
||||
|
||||
_eventInCache = (key) ->
|
||||
curCache = _getEventCache()
|
||||
curCache[key] || false
|
||||
|
||||
_addEventToCache = (key) ->
|
||||
curCache = _getEventCache()
|
||||
curCache[key] = true
|
||||
|
||||
localStorage CACHE_KEY, curCache
|
||||
|
||||
App.factory "event_tracking", ->
|
||||
return {
|
||||
send: (category, action, label, value)->
|
||||
ga('send', 'event', category, action, label, value)
|
||||
|
||||
sendCountly: (key, segmentation) ->
|
||||
eventData = { key }
|
||||
eventData.segmentation = segmentation if segmentation?
|
||||
Countly?.q.push([ "add_event", eventData ])
|
||||
|
||||
sendCountlySampled: (key, segmentation) ->
|
||||
@sendCountly key, segmentation if Math.random() < .01
|
||||
|
||||
sendCountlyOnce: (key, segmentation) ->
|
||||
if ! _eventInCache(key)
|
||||
_addEventToCache(key)
|
||||
@sendCountly key, segmentation
|
||||
}
|
||||
|
||||
# App.directive "countlyTrack", () ->
|
||||
# return {
|
||||
# restrict: "A"
|
||||
# scope: false,
|
||||
# link: (scope, el, attrs) ->
|
||||
# eventKey = attrs.countlyTrack
|
||||
# if (eventKey?)
|
||||
# el.on "click", () ->
|
||||
# console.log eventKey
|
||||
# }
|
||||
|
||||
#header
|
||||
$('.navbar a').on "click", (e)->
|
||||
href = $(e.target).attr("href")
|
||||
if href?
|
||||
ga('send', 'event', 'navigation', 'top menu bar', href)
|
||||
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
define [
|
||||
"base"
|
||||
"services/algolia-search"
|
||||
], (App) ->
|
||||
|
||||
App.factory "algoliawiki", ->
|
||||
if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
|
||||
client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
|
||||
index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
|
||||
return index
|
||||
|
||||
App.controller "SearchWikiController", ($scope, algoliawiki, _, $modal) ->
|
||||
algolia = algoliawiki
|
||||
App.controller "SearchWikiController", ($scope, algoliaSearch, _, $modal) ->
|
||||
$scope.hits = []
|
||||
|
||||
$scope.clearSearchText = ->
|
||||
|
@ -54,7 +48,7 @@ define [
|
|||
updateHits []
|
||||
return
|
||||
|
||||
algolia.search query, (err, response)->
|
||||
algoliaSearch.searchWiki query, (err, response)->
|
||||
if response.hits.length == 0
|
||||
updateHits []
|
||||
else
|
||||
|
|
|
@ -2,7 +2,7 @@ define [
|
|||
"base"
|
||||
], (App)->
|
||||
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack)->
|
||||
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
|
||||
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
|
@ -51,6 +51,8 @@ define [
|
|||
.done()
|
||||
|
||||
pricing.on "change", =>
|
||||
event_tracking.sendCountly "subscription-form", { plan : pricing.items.plan.code }
|
||||
|
||||
$scope.planName = pricing.items.plan.name
|
||||
$scope.price = pricing.price
|
||||
$scope.trialLength = pricing.items.plan.trial?.length
|
||||
|
@ -115,9 +117,25 @@ define [
|
|||
currencyCode:pricing.items.currency
|
||||
plan_code:pricing.items.plan.code
|
||||
coupon_code:pricing.items?.coupon?.code || ""
|
||||
isPaypal: $scope.paymentMethod == 'paypal'
|
||||
address:
|
||||
address1: $scope.data.address1
|
||||
address2: $scope.data.address2
|
||||
country: $scope.data.country
|
||||
state: $scope.data.state
|
||||
postal_code: $scope.data.postal_code
|
||||
|
||||
event_tracking.sendCountly "subscription-form-submitted", {
|
||||
currencyCode : postData.subscriptionDetails.currencyCode,
|
||||
plan_code : postData.subscriptionDetails.plan_code,
|
||||
coupon_code : postData.subscriptionDetails.coupon_code,
|
||||
isPaypal : postData.subscriptionDetails.isPaypal
|
||||
}
|
||||
|
||||
$http.post("/user/subscription/create", postData)
|
||||
.success (data, status, headers)->
|
||||
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
|
||||
event_tracking.sendCountly "subscription-submission-success"
|
||||
window.location.href = "/user/subscription/thank-you"
|
||||
.error (data, status, headers)->
|
||||
$scope.processing = false
|
||||
|
|
|
@ -4,7 +4,6 @@ define [
|
|||
|
||||
App.controller 'LeftHandMenuPromoController', ($scope) ->
|
||||
|
||||
$scope.showDatajoy = Math.random() < 0.5
|
||||
$scope.hasProjects = window.data.projects.length > 0
|
||||
$scope.userHasSubscription = window.userHasSubscription
|
||||
$scope.userHasNoSubscription = window.userHasNoSubscription
|
||||
$scope.randomView = _.shuffle(["default", "dropbox", "github"])[0]
|
|
@ -1,15 +1,9 @@
|
|||
define [
|
||||
"base"
|
||||
"services/algolia-search"
|
||||
], (App) ->
|
||||
|
||||
App.factory "algoliawiki", ->
|
||||
if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
|
||||
client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
|
||||
index = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
|
||||
return index
|
||||
|
||||
App.controller "SearchWikiController", ($scope, algoliawiki, _) ->
|
||||
algolia = algoliawiki
|
||||
App.controller "SearchWikiController", ($scope, algoliaSearch, _) ->
|
||||
$scope.hits = []
|
||||
|
||||
$scope.clearSearchText = ->
|
||||
|
@ -54,7 +48,7 @@ define [
|
|||
updateHits []
|
||||
return
|
||||
|
||||
algolia.search query, (err, response)->
|
||||
algoliaSearch.searchWiki query, (err, response)->
|
||||
if response.hits.length == 0
|
||||
updateHits []
|
||||
else
|
||||
|
|
14
services/web/public/coffee/services/algolia-search.coffee
Normal file
|
@ -0,0 +1,14 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "algoliaSearch", ->
|
||||
if window.sharelatex?.algolia? and window.sharelatex.algolia?.indexes?.wiki?
|
||||
client = new AlgoliaSearch(window.sharelatex.algolia?.app_id, window.sharelatex.algolia?.api_key)
|
||||
wikiIdx = client.initIndex(window.sharelatex.algolia?.indexes?.wiki)
|
||||
kbIdx = client.initIndex(window.sharelatex.algolia?.indexes?.kb)
|
||||
|
||||
service =
|
||||
searchWiki: wikiIdx.search.bind(wikiIdx)
|
||||
searchKB: kbIdx.search.bind(kbIdx)
|
||||
|
||||
return service
|
|
@ -0,0 +1,58 @@
|
|||
define [
|
||||
"base"
|
||||
], (App) ->
|
||||
App.factory "logHintsFeedback", ($http, $q) ->
|
||||
hintsFeedbackFormAPIHash = "rl4xgvr1v5t64a"
|
||||
idStampVal = "OPkEWEFHUFAm7hKlraQMhiOXQabafWo8NipRvLT397w="
|
||||
hintFieldAPIId = "3"
|
||||
reasonFieldAPIId = "1"
|
||||
reasonOtherFieldAPIId = "1_other_other"
|
||||
submitEndpoint = "https://sharelatex.wufoo.eu/forms/#{ hintsFeedbackFormAPIHash }/#public"
|
||||
|
||||
feedbackOpts =
|
||||
DIDNT_UNDERSTAND: "didnt_understand"
|
||||
NOT_APPLICABLE: "not_applicable"
|
||||
INCORRECT: "incorrect"
|
||||
OTHER: "other"
|
||||
|
||||
createRequest = (hintId, feedbackOpt, feedbackOtherVal = "") ->
|
||||
formData = new FormData()
|
||||
|
||||
formData.append "Field#{ hintFieldAPIId }", hintId
|
||||
formData.append "Field#{ reasonFieldAPIId }", feedbackOpt
|
||||
formData.append "idstamp", idStampVal
|
||||
|
||||
# Allow user to specify "other" without any extra details; an empty string
|
||||
# would trigger an error submitting.
|
||||
if feedbackOpt == feedbackOpts.OTHER and feedbackOtherVal == ""
|
||||
formData.append "Field#{ reasonOtherFieldAPIId }", "#{ feedbackOpts.OTHER } empty"
|
||||
else
|
||||
formData.append "Field#{ reasonOtherFieldAPIId }", feedbackOtherVal
|
||||
|
||||
req =
|
||||
method: 'POST'
|
||||
url: submitEndpoint
|
||||
# This will effectively disable Angular's default serialization mechanisms,
|
||||
# forcing the XHR to be done with whatever data we provide (in this case,
|
||||
# form data). Without this, Angular will forcefully try to serialize data
|
||||
# to JSON.
|
||||
transformRequest: angular.identity
|
||||
data: formData
|
||||
headers :
|
||||
# This will tell Angular to use the browser-provided value, which is
|
||||
# computed according to the data being sent (in this case, multipart
|
||||
# form + browser-specific multipart boundary). Without this, Angular
|
||||
# will set JSON.
|
||||
"Content-Type": undefined
|
||||
|
||||
return req
|
||||
|
||||
submitFeedback = (hintId, feedbackOpt, feedbackOtherVal = "") ->
|
||||
submitRequest = createRequest hintId, feedbackOpt, feedbackOtherVal
|
||||
$http(submitRequest)
|
||||
|
||||
service =
|
||||
feedbackOpts: feedbackOpts
|
||||
submitFeedback: submitFeedback
|
||||
|
||||
return service
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
services/web/public/img/about/paulo_reis.jpg
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 592 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 5.6 KiB |